Преглед изворни кода

fix: BUGFIX: Fixes the issue with the Docker Hub login process and improves error handling in the UI.

dqzboy пре 6 месеци
родитељ
комит
46f067b495
72 измењених фајлова са 15377 додато и 4031 уклоњено
  1. 69 0
      hubcmdui/README.md
  2. 186 0
      hubcmdui/app.js
  3. 72 0
      hubcmdui/cleanup.js
  4. 1148 0
      hubcmdui/compatibility-layer.js
  5. 54 0
      hubcmdui/config.js
  6. 20 23
      hubcmdui/config.json
  7. 1 0
      hubcmdui/config/menu.json
  8. 8 0
      hubcmdui/config/monitoring.json
  9. 29 0
      hubcmdui/data/config.json
  10. 11 1
      hubcmdui/docker-compose.yaml
  11. BIN
      hubcmdui/documentation/.DS_Store
  12. 0 1
      hubcmdui/documentation/1724594777670.json
  13. 0 1
      hubcmdui/documentation/1737713570870.json
  14. 0 1
      hubcmdui/documentation/1737713707391.json
  15. 7 0
      hubcmdui/documentation/1743542841590.json
  16. 7 0
      hubcmdui/documentation/1743543376091.json
  17. 7 0
      hubcmdui/documentation/1743543400369.json
  18. 83 0
      hubcmdui/download-images.js
  19. 86 0
      hubcmdui/init-dirs.js
  20. 340 182
      hubcmdui/logger.js
  21. 90 0
      hubcmdui/middleware/auth.js
  22. 26 0
      hubcmdui/middleware/client-error.js
  23. 13 0
      hubcmdui/models/MenuItem.js
  24. 37 11
      hubcmdui/package.json
  25. 155 0
      hubcmdui/routes/auth.js
  26. 224 0
      hubcmdui/routes/config.js
  27. 146 0
      hubcmdui/routes/docker.js
  28. 65 0
      hubcmdui/routes/dockerhub.js
  29. 537 0
      hubcmdui/routes/documentation.js
  30. 48 0
      hubcmdui/routes/health.js
  31. 78 0
      hubcmdui/routes/index.js
  32. 92 0
      hubcmdui/routes/login.js
  33. 193 0
      hubcmdui/routes/monitoring.js
  34. 55 0
      hubcmdui/routes/routeLoader.js
  35. 590 0
      hubcmdui/routes/system.js
  36. 104 0
      hubcmdui/routes/systemStatus.js
  37. 163 0
      hubcmdui/scripts/diagnostics.js
  38. 25 0
      hubcmdui/scripts/init-menu.js
  39. 315 0
      hubcmdui/scripts/init-system.js
  40. 333 0
      hubcmdui/server-utils.js
  41. 149 1724
      hubcmdui/server.js
  42. 47 0
      hubcmdui/services/configService.js
  43. 290 0
      hubcmdui/services/dockerHubService.js
  44. 476 0
      hubcmdui/services/dockerService.js
  45. 324 0
      hubcmdui/services/documentationService.js
  46. 331 0
      hubcmdui/services/monitoringService.js
  47. 52 0
      hubcmdui/services/networkService.js
  48. 103 0
      hubcmdui/services/notificationService.js
  49. 55 0
      hubcmdui/services/systemService.js
  50. 175 0
      hubcmdui/services/userService.js
  51. 58 0
      hubcmdui/start-diagnostic.js
  52. 3 1
      hubcmdui/users.json
  53. BIN
      hubcmdui/web/.DS_Store
  54. 518 1882
      hubcmdui/web/admin.html
  55. 53 0
      hubcmdui/web/compatibility-layer.js
  56. 396 0
      hubcmdui/web/css/admin.css
  57. 662 0
      hubcmdui/web/css/custom.css
  58. 1 0
      hubcmdui/web/data/documentation/index.json
  59. BIN
      hubcmdui/web/images/login-bg.jpg
  60. 372 191
      hubcmdui/web/index.html
  61. 495 0
      hubcmdui/web/js/app.js
  62. 124 0
      hubcmdui/web/js/auth.js
  63. 499 0
      hubcmdui/web/js/core.js
  64. 681 0
      hubcmdui/web/js/dockerManager.js
  65. 958 0
      hubcmdui/web/js/documentManager.js
  66. 85 0
      hubcmdui/web/js/error-handler.js
  67. 573 0
      hubcmdui/web/js/menuManager.js
  68. 93 0
      hubcmdui/web/js/networkTest.js
  69. 1121 0
      hubcmdui/web/js/systemStatus.js
  70. 260 0
      hubcmdui/web/js/userCenter.js
  71. 224 0
      hubcmdui/web/services/documentationService.js
  72. 782 13
      hubcmdui/web/style.css

+ 69 - 0
hubcmdui/README.md

@@ -26,6 +26,75 @@
 
 ---
 
+## 🔧 日志系统说明
+
+本项目实现了生产级别的日志系统,支持以下特性:
+
+### 日志级别
+
+支持的日志级别从低到高依次为:
+- `TRACE`: 最详细的追踪信息,用于开发调试
+- `DEBUG`: 调试信息,包含详细的程序执行流程
+- `INFO`: 一般信息,默认级别
+- `SUCCESS`: 成功信息,通常用于标记重要操作的成功完成
+- `WARN`: 警告信息,表示潜在的问题
+- `ERROR`: 错误信息,表示操作失败但程序仍可继续运行
+- `FATAL`: 致命错误,通常会导致程序退出
+
+### 环境变量配置
+
+可通过环境变量调整日志行为:
+
+```bash
+# 设置日志级别
+export LOG_LEVEL=INFO  # 可选值: TRACE, DEBUG, INFO, SUCCESS, WARN, ERROR, FATAL
+
+# 启用简化日志输出(减少浏览器请求详细信息)
+export SIMPLE_LOGS=true
+
+# 启用详细日志记录(包含请求体、查询参数等)
+export DETAILED_LOGS=true
+
+# 启用错误堆栈跟踪
+export SHOW_STACK=true
+
+# 禁用文件日志记录
+export LOG_FILE_ENABLED=false
+
+# 禁用控制台日志输出
+export LOG_CONSOLE_ENABLED=false
+
+# 设置日志文件大小上限(MB)
+export LOG_MAX_SIZE=10
+
+# 设置保留的日志文件数量
+export LOG_MAX_FILES=14
+```
+
+### Docker运行时配置
+
+使用Docker运行时,可以通过环境变量传递配置:
+
+```bash
+docker run -d \
+  -v /var/run/docker.sock:/var/run/docker.sock \
+  -p 30080:3000 \
+  -e LOG_LEVEL=INFO \
+  -e SIMPLE_LOGS=true \
+  -e LOG_MAX_FILES=7 \
+  --name hubcmdui-server \
+  dqzboy/hubcmd-ui
+```
+
+### 日志文件轮转
+
+系统自动实现日志文件轮转:
+- 单个日志文件超过设定大小(默认10MB)会自动创建新文件
+- 自动保留指定数量(默认14个)的最新日志文件
+- 日志文件存储在`logs`目录下,格式为`app-YYYY-MM-DD.log`
+
+---
+
 ## 📝 源码构建运行
 #### 1. 克隆项目
 ```bash

+ 186 - 0
hubcmdui/app.js

@@ -0,0 +1,186 @@
+#!/usr/bin/env node
+
+/**
+ * 应用主入口文件 - 启动服务器并初始化所有组件
+ */
+
+// 记录服务器启动时间 - 最先执行这行代码,确保第一时间记录
+global.serverStartTime = Date.now();
+
+const express = require('express');
+const session = require('express-session');
+const path = require('path');
+const http = require('http');
+const logger = require('./logger');
+const { ensureDirectoriesExist } = require('./init-dirs');
+const registerRoutes = require('./routes');
+const { requireLogin, sessionActivity, sanitizeRequestBody, securityHeaders } = require('./middleware/auth');
+
+// 记录服务器启动时间到日志
+console.log(`服务器启动,时间戳: ${global.serverStartTime}`);
+logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`);
+
+// 添加 session 文件存储模块 - 先导入session-file-store并创建对象
+const FileStore = require('session-file-store')(session);
+
+// 确保目录结构存在
+ensureDirectoriesExist().catch(err => {
+  logger.error('创建必要目录失败:', err);
+  process.exit(1);
+});
+
+// 初始化Express应用 - 确保正确初始化
+const app = express();
+const server = http.createServer(app);
+
+// 基本中间件配置
+app.use(express.json());
+app.use(express.urlencoded({ extended: true }));
+app.use(express.static(path.join(__dirname, 'web')));
+// 添加对documentation目录的静态访问
+app.use('/documentation', express.static(path.join(__dirname, 'documentation')));
+app.use(sessionActivity);
+app.use(sanitizeRequestBody);
+app.use(securityHeaders);
+
+// 会话配置
+app.use(session({
+  secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key',
+  resave: false,
+  saveUninitialized: false,
+  cookie: { 
+    secure: process.env.NODE_ENV === 'production',
+    maxAge: 24 * 60 * 60 * 1000 // 24小时
+  },
+  store: new FileStore({
+    path: path.join(__dirname, 'data', 'sessions'),
+    ttl: 86400
+  })
+}));
+
+// 添加一个中间件来检查API请求的会话状态
+app.use('/api', (req, res, next) => {
+  // 这些API端点不需要登录
+  const publicEndpoints = [
+    '/api/login', 
+    '/api/logout', 
+    '/api/check-session', 
+    '/api/health',
+    '/api/system-status',
+    '/api/system-resource-details',
+    '/api/menu-items',
+    '/api/config',
+    '/api/monitoring-config',
+    '/api/documentation',
+    '/api/documentation/file'
+  ];
+  
+  // 如果是公共API或用户已登录,则继续
+  if (publicEndpoints.includes(req.path) || 
+      publicEndpoints.some(endpoint => req.path.startsWith(endpoint)) || 
+      (req.session && req.session.user)) {
+    return next();
+  }
+  
+  // 否则返回401未授权
+  logger.warn(`未授权访问: ${req.path}`);
+  return res.status(401).json({ error: 'Unauthorized' });
+});
+
+// 导入并注册所有路由
+registerRoutes(app);
+
+// 默认路由
+app.get('/', (req, res) => {
+  res.sendFile(path.join(__dirname, 'web', 'index.html'));
+});
+
+app.get('/admin', (req, res) => {
+  res.sendFile(path.join(__dirname, 'web', 'admin.html'));
+});
+
+// 404处理
+app.use((req, res) => {
+  res.status(404).json({ error: 'Not Found' });
+});
+
+// 错误处理中间件
+app.use((err, req, res, next) => {
+  logger.error('应用错误:', err);
+  res.status(500).json({ error: '服务器内部错误', details: err.message });
+});
+
+// 启动服务器
+const PORT = process.env.PORT || 3000;
+server.listen(PORT, async () => {
+  logger.info(`服务器已启动并监听端口 ${PORT}`);
+  
+  try {
+    // 确保目录存在
+    await ensureDirectoriesExist();
+    logger.success('系统初始化完成');
+  } catch (error) {
+    logger.error('系统初始化失败:', error);
+  }
+});
+
+// 注册进程事件处理
+process.on('SIGINT', () => {
+  logger.info('接收到中断信号,正在关闭服务...');
+  server.close(() => {
+    logger.info('服务器已关闭');
+    process.exit(0);
+  });
+});
+
+process.on('SIGTERM', () => {
+  logger.info('接收到终止信号,正在关闭服务...');
+  server.close(() => {
+    logger.info('服务器已关闭');
+    process.exit(0);
+  });
+});
+
+module.exports = { app, server };
+
+// 路由注册函数
+function registerRoutes(app) {
+  try {
+    logger.info('开始注册路由...');
+    
+    // API端点
+    app.use('/api', [
+      require('./routes/index'),
+      require('./routes/docker'),
+      require('./routes/docs'),
+      require('./routes/users'),
+      require('./routes/menu'),
+      require('./routes/server')
+    ]);
+    logger.info('基本API路由已注册');
+    
+    // 系统路由 - 函数式注册
+    const systemRouter = require('./routes/system');
+    app.use('/api/system', systemRouter);
+    logger.info('系统路由已注册');
+    
+    // 认证路由 - 直接使用Router实例
+    const authRouter = require('./routes/auth');
+    app.use('/api', authRouter);
+    logger.info('认证路由已注册');
+    
+    // 配置路由 - 函数式注册
+    const configRouter = require('./routes/config');
+    if (typeof configRouter === 'function') {
+      logger.info('配置路由是一个函数,正在注册...');
+      configRouter(app);
+      logger.info('配置路由已注册');
+    } else {
+      logger.error('配置路由不是一个函数,无法注册', typeof configRouter);
+    }
+    
+    logger.success('✓ 所有路由已注册');
+  } catch (error) {
+    logger.error('路由注册失败:', error);
+  }
+}

+ 72 - 0
hubcmdui/cleanup.js

@@ -0,0 +1,72 @@
+const logger = require('./logger');
+
+// 处理未捕获的异常
+process.on('uncaughtException', (error) => {
+  logger.error('未捕获的异常:', error);
+  // 打印完整的堆栈跟踪以便调试
+  console.error('错误堆栈:', error.stack);
+  // 不立即退出,以便日志能够被写入
+  setTimeout(() => {
+    process.exit(1);
+  }, 1000);
+});
+
+// 处理未处理的Promise拒绝
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error('未处理的Promise拒绝:', reason);
+  // 打印堆栈跟踪(如果可用)
+  if (reason instanceof Error) {
+    console.error('Promise拒绝堆栈:', reason.stack);
+  }
+});
+
+// 处理退出信号
+process.on('SIGINT', gracefulShutdown);
+process.on('SIGTERM', gracefulShutdown);
+
+// 优雅退出函数
+function gracefulShutdown() {
+  logger.info('接收到退出信号,正在关闭...');
+  
+  // 这里可以添加清理代码,如关闭数据库连接等
+  try {
+    // 关闭任何可能的资源
+    try {
+      const docker = require('./services/dockerService').getDockerConnection();
+      if (docker) {
+        logger.info('正在关闭Docker连接...');
+        // 如果有活动的Docker连接,可能需要执行一些清理
+      }
+    } catch (err) {
+      // 忽略错误,可能服务未初始化
+      logger.debug('Docker服务未初始化,跳过清理');
+    }
+    
+    // 清理监控间隔
+    try {
+      const monitoringService = require('./services/monitoringService');
+      if (monitoringService.stopMonitoring) {
+        logger.info('正在停止容器监控...');
+        monitoringService.stopMonitoring();
+      }
+    } catch (err) {
+      // 忽略错误,可能服务未初始化
+      logger.debug('监控服务未初始化,跳过清理');
+    }
+    
+    logger.info('所有资源已清理完毕,正在退出...');
+  } catch (error) {
+    logger.error('退出过程中出现错误:', error);
+  }
+  
+  setTimeout(() => {
+    logger.info('干净退出完成');
+    process.exit(0);
+  }, 1000);
+}
+
+logger.info('错误处理和清理脚本已加载');
+
+module.exports = {
+  gracefulShutdown
+};

+ 1148 - 0
hubcmdui/compatibility-layer.js

@@ -0,0 +1,1148 @@
+/**
+ * 兼容层 - 确保旧版API接口继续工作
+ */
+const logger = require('./logger');
+const { requireLogin } = require('./middleware/auth');
+const { execCommand } = require('./server-utils');
+const os = require('os');
+const { exec } = require('child_process');
+const util = require('util');
+const execPromise = util.promisify(exec);
+
+module.exports = function(app) {
+  logger.info('加载API兼容层...');
+  
+  // 会话检查接口
+  app.get('/api/check-session', (req, res) => {
+    if (req.session && req.session.user) {
+      res.json({ authenticated: true, user: req.session.user });
+    } else {
+      res.json({ authenticated: false });
+    }
+  });
+  
+  // 添加Docker状态检查接口,并使用 requireLogin 中间件
+  app.get('/api/docker/status', requireLogin, async (req, res) => {
+    try {
+      const dockerService = require('./services/dockerService');
+      const dockerStatus = await dockerService.checkDockerAvailability();
+      res.json({ isRunning: dockerStatus });
+    } catch (error) {
+      logger.error('检查Docker状态失败:', error);
+      res.status(500).json({ error: '检查Docker状态失败', details: error.message });
+    }
+  });
+  
+  // 验证码接口
+  app.get('/api/captcha', (req, res) => {
+    try {
+      const num1 = Math.floor(Math.random() * 10);
+      const num2 = Math.floor(Math.random() * 10);
+      const captcha = `${num1} + ${num2} = ?`;
+      req.session.captcha = num1 + num2;
+      res.json({ captcha });
+    } catch (error) {
+      logger.error('生成验证码失败:', error);
+      res.status(500).json({ error: '生成验证码失败' });
+    }
+  });
+  
+  // 停止容器列表接口
+  app.get('/api/stopped-containers', requireLogin, async (req, res) => {
+    try {
+      const monitoringService = require('./services/monitoringService');
+      const stoppedContainers = await monitoringService.getStoppedContainers();
+      res.json(stoppedContainers);
+    } catch (error) {
+      logger.error('获取已停止容器列表失败:', error);
+      res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
+    }
+  });
+  
+  // 修复Docker Hub搜索接口 - 直接使用axios请求,避免dockerHubService的依赖问题
+  app.get('/api/dockerhub/search', async (req, res) => {
+    try {
+      const axios = require('axios');
+      const term = req.query.term;
+      const page = req.query.page || 1;
+      
+      if (!term) {
+        return res.status(400).json({ error: '搜索词不能为空' });
+      }
+      
+      logger.info(`搜索Docker Hub: ${term} (页码: ${page})`);
+      
+      const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`;
+      const response = await axios.get(url, {
+        timeout: 15000,
+        headers: {
+          'User-Agent': 'DockerHubSearchClient/1.0',
+          'Accept': 'application/json'
+        }
+      });
+      
+      res.json(response.data);
+    } catch (error) {
+      logger.error('搜索Docker Hub失败:', error.message || error);
+      res.status(500).json({ 
+        error: '搜索失败', 
+        details: error.message || '未知错误',
+        retryable: true 
+      });
+    }
+  });
+  
+  // Docker Hub 标签计数接口
+  app.get('/api/dockerhub/tag-count', async (req, res) => {
+    try {
+      const axios = require('axios');
+      const name = req.query.name;
+      const isOfficial = req.query.official === 'true';
+      
+      if (!name) {
+        return res.status(400).json({ error: '镜像名称不能为空' });
+      }
+      
+      const fullImageName = isOfficial ? `library/${name}` : name;
+      const apiUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags/?page_size=1`;
+      
+      logger.info(`获取标签计数: ${fullImageName}`);
+      
+      const response = await axios.get(apiUrl, {
+        timeout: 15000,
+        headers: {
+          'User-Agent': 'DockerHubSearchClient/1.0',
+          'Accept': 'application/json'
+        }
+      });
+      
+      res.json({
+        count: response.data.count,
+        recommended_mode: response.data.count > 500 ? 'paginated' : 'full'
+      });
+    } catch (error) {
+      logger.error('获取标签计数失败:', error.message || error);
+      res.status(500).json({ 
+        error: '获取标签计数失败', 
+        details: error.message || '未知错误',
+        retryable: true 
+      });
+    }
+  });
+  
+  // Docker Hub 标签接口
+  app.get('/api/dockerhub/tags', async (req, res) => {
+    try {
+      const axios = require('axios');
+      const imageName = req.query.name;
+      const isOfficial = req.query.official === 'true';
+      const page = parseInt(req.query.page) || 1;
+      const page_size = parseInt(req.query.page_size) || 25;
+      const getAllTags = req.query.all === 'true';
+      
+      if (!imageName) {
+        return res.status(400).json({ error: '镜像名称不能为空' });
+      }
+      
+      const fullImageName = isOfficial ? `library/${imageName}` : imageName;
+      logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 每页数量: ${page_size}, 获取全部: ${getAllTags}`);
+      
+      // 如果请求所有标签,需要递归获取所有页
+      if (getAllTags) {
+        // 暂不实现全部获取,返回错误
+        return res.status(400).json({ error: '获取全部标签功能暂未实现,请使用分页获取' });
+      } else {
+        // 获取特定页的标签
+        const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${page_size}`;
+        
+        const tagsResponse = await axios.get(tagsUrl, {
+          timeout: 15000,
+          headers: {
+            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
+          }
+        });
+        
+        // 检查响应数据有效性
+        if (!tagsResponse.data || typeof tagsResponse.data !== 'object') {
+          logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`);
+          return res.status(500).json({ error: '响应数据格式不正确' });
+        }
+        
+        if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) {
+          logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`);
+          return res.status(500).json({ error: '没有找到有效的标签数据' });
+        }
+        
+        // 过滤掉无效平台信息
+        const cleanedResults = tagsResponse.data.results.map(tag => {
+          if (tag.images && Array.isArray(tag.images)) {
+            tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
+          }
+          return tag;
+        });
+        
+        return res.json({
+          ...tagsResponse.data,
+          results: cleanedResults
+        });
+      }
+    } catch (error) {
+      logger.error('获取标签列表失败:', error.message || error);
+      res.status(500).json({ 
+        error: '获取标签列表失败', 
+        details: error.message || '未知错误',
+        retryable: true 
+      });
+    }
+  });
+  
+  // 文档接口
+  app.get('/api/documentation', async (req, res) => {
+    try {
+      const docService = require('./services/documentationService');
+      const documents = await docService.getPublishedDocuments();
+      res.json(documents);
+    } catch (error) {
+      logger.error('获取已发布文档失败:', error);
+      res.status(500).json({ error: '获取文档失败', details: error.message });
+    }
+  });
+  
+  // 监控配置接口
+  app.get('/api/monitoring-config', async (req, res) => {
+    try {
+      logger.info('兼容层处理监控配置请求');
+      const fs = require('fs').promises;
+      const path = require('path');
+      
+      // 监控配置文件路径
+      const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
+      
+      // 确保配置文件存在
+      try {
+        await fs.access(CONFIG_FILE);
+      } catch (err) {
+        // 文件不存在,创建默认配置
+        const defaultConfig = {
+          isEnabled: false,
+          notificationType: 'wechat',
+          webhookUrl: '',
+          telegramToken: '',
+          telegramChatId: '',
+          monitorInterval: 60
+        };
+        
+        await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
+        await fs.writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf8');
+        return res.json(defaultConfig);
+      }
+      
+      // 文件存在,读取配置
+      const data = await fs.readFile(CONFIG_FILE, 'utf8');
+      res.json(JSON.parse(data));
+    } catch (err) {
+      logger.error('获取监控配置失败:', err);
+      res.status(500).json({ error: '获取监控配置失败' });
+    }
+  });
+  
+  // 保存监控配置接口
+  app.post('/api/monitoring-config', async (req, res) => {
+    try {
+      logger.info('兼容层处理保存监控配置请求');
+      const fs = require('fs').promises;
+      const path = require('path');
+      
+      const { 
+        notificationType, 
+        webhookUrl, 
+        telegramToken, 
+        telegramChatId, 
+        monitorInterval,
+        isEnabled
+      } = req.body;
+      
+      // 简单验证
+      if (notificationType === 'wechat' && !webhookUrl) {
+        return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
+      }
+      
+      if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+        return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
+      }
+      
+      // 监控配置文件路径
+      const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
+      
+      // 确保配置文件存在
+      let config = {
+        isEnabled: false,
+        notificationType: 'wechat',
+        webhookUrl: '',
+        telegramToken: '',
+        telegramChatId: '',
+        monitorInterval: 60
+      };
+      
+      try {
+        const data = await fs.readFile(CONFIG_FILE, 'utf8');
+        config = JSON.parse(data);
+      } catch (err) {
+        // 如果读取失败,使用默认配置
+        logger.warn('读取监控配置失败,将使用默认配置:', err);
+      }
+      
+      // 更新配置
+      const updatedConfig = {
+        ...config,
+        notificationType,
+        webhookUrl: webhookUrl || '',
+        telegramToken: telegramToken || '',
+        telegramChatId: telegramChatId || '',
+        monitorInterval: parseInt(monitorInterval, 10) || 60,
+        isEnabled: isEnabled !== undefined ? isEnabled : config.isEnabled
+      };
+      
+      await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
+      await fs.writeFile(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2), 'utf8');
+      
+      res.json({ success: true, message: '监控配置已保存' });
+      
+      // 通知监控服务重新加载配置
+      if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
+        global.monitoringService.reload();
+      }
+    } catch (err) {
+      logger.error('保存监控配置失败:', err);
+      res.status(500).json({ error: '保存监控配置失败' });
+    }
+  });
+  
+  // 获取单个文档接口
+  app.get('/api/documentation/:id', async (req, res) => {
+    try {
+      const docService = require('./services/documentationService');
+      const document = await docService.getDocument(req.params.id);
+      
+      // 如果文档不是发布状态,只有已登录用户才能访问
+      if (!document.published && !req.session.user) {
+        return res.status(403).json({ error: '没有权限访问该文档' });
+      }
+      
+      res.json(document);
+    } catch (error) {
+      logger.error(`获取文档 ID:${req.params.id} 失败:`, error);
+      if (error.code === 'ENOENT') {
+        return res.status(404).json({ error: '文档不存在' });
+      }
+      res.status(500).json({ error: '获取文档失败', details: error.message });
+    }
+  });
+  
+  // 文档列表接口
+  app.get('/api/documentation-list', requireLogin, async (req, res) => {
+    try {
+      const docService = require('./services/documentationService');
+      const documents = await docService.getDocumentationList();
+      res.json(documents);
+    } catch (error) {
+      logger.error('获取文档列表失败:', error);
+      res.status(500).json({ error: '获取文档列表失败', details: error.message });
+    }
+  });
+  
+  // 切换监控状态接口
+  app.post('/api/toggle-monitoring', async (req, res) => {
+    try {
+      logger.info('兼容层处理切换监控状态请求');
+      const fs = require('fs').promises;
+      const path = require('path');
+      
+      const { isEnabled } = req.body;
+      
+      // 监控配置文件路径
+      const CONFIG_FILE = path.join(__dirname, './config/monitoring.json');
+      
+      // 确保配置文件存在
+      let config = {
+        isEnabled: false,
+        notificationType: 'wechat',
+        webhookUrl: '',
+        telegramToken: '',
+        telegramChatId: '',
+        monitorInterval: 60
+      };
+      
+      try {
+        const data = await fs.readFile(CONFIG_FILE, 'utf8');
+        config = JSON.parse(data);
+      } catch (err) {
+        // 如果读取失败,使用默认配置
+        logger.warn('读取监控配置失败,将使用默认配置:', err);
+      }
+      
+      // 更新启用状态
+      config.isEnabled = !!isEnabled;
+      
+      await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
+      await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
+      
+      res.json({ 
+        success: true, 
+        message: `监控已${isEnabled ? '启用' : '禁用'}`
+      });
+      
+      // 通知监控服务重新加载配置
+      if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
+        global.monitoringService.reload();
+      }
+    } catch (err) {
+      logger.error('切换监控状态失败:', err);
+      res.status(500).json({ error: '切换监控状态失败' });
+    }
+  });
+  
+  // 测试通知接口
+  app.post('/api/test-notification', async (req, res) => {
+    try {
+      logger.info('兼容层处理测试通知请求');
+      
+      const { 
+        notificationType, 
+        webhookUrl, 
+        telegramToken, 
+        telegramChatId
+      } = req.body;
+      
+      // 简单验证
+      if (notificationType === 'wechat' && !webhookUrl) {
+        return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
+      }
+      
+      if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+        return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
+      }
+      
+      // 发送测试通知
+      const notifier = require('./services/notificationService');
+      const testMessage = {
+        title: '测试通知',
+        content: '这是一条测试通知,如果您收到这条消息,说明您的通知配置工作正常。',
+        time: new Date().toLocaleString()
+      };
+      
+      await notifier.sendNotification(testMessage, {
+        type: notificationType,
+        webhookUrl,
+        telegramToken,
+        telegramChatId
+      });
+      
+      res.json({ success: true, message: '测试通知已发送' });
+    } catch (err) {
+      logger.error('发送测试通知失败:', err);
+      res.status(500).json({ error: '发送测试通知失败: ' + err.message });
+    }
+  });
+  
+  // 获取已停止的容器接口
+  app.get('/api/stopped-containers', requireLogin, async (req, res) => {
+    try {
+      logger.info('兼容层处理获取已停止容器请求');
+      const { exec } = require('child_process');
+      const util = require('util');
+      const execPromise = util.promisify(exec);
+      
+      const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Status}}"');
+      
+      const containers = stdout.trim().split('\n')
+        .filter(line => line.trim())
+        .map(line => {
+          const [id, name, ...statusParts] = line.split('\t');
+          return {
+            id: id.substring(0, 12),
+            name,
+            status: statusParts.join(' ')
+          };
+        });
+      
+      res.json(containers);
+    } catch (err) {
+      logger.error('获取已停止容器失败:', err);
+      res.status(500).json({ error: '获取已停止容器失败', details: err.message });
+    }
+  });
+  
+  // 系统状态接口
+  app.get('/api/system-status', requireLogin, async (req, res) => {
+    try {
+      const systemRouter = require('./routes/system');
+      return await systemRouter.getSystemStats(req, res);
+    } catch (error) {
+      logger.error('获取系统状态失败:', error);
+      res.status(500).json({ error: '获取系统状态失败', details: error.message });
+    }
+  });
+  
+  // Docker容器状态接口
+  app.get('/api/docker-status', async (req, res) => {
+    try {
+      const dockerService = require('./services/dockerService');
+      const containerStatus = await dockerService.getContainersStatus();
+      res.json(containerStatus);
+    } catch (error) {
+      logger.error('获取Docker状态失败:', error);
+      res.status(500).json({ error: '获取Docker状态失败', details: error.message });
+    }
+  });
+  
+  // 单个容器状态接口
+  app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
+    try {
+      const dockerService = require('./services/dockerService');
+      const containerInfo = await dockerService.getContainerStatus(req.params.id);
+      res.json(containerInfo);
+    } catch (error) {
+      logger.error('获取容器状态失败:', error);
+      res.status(500).json({ error: '获取容器状态失败', details: error.message });
+    }
+  });
+  
+  // 添加Docker容器操作API兼容层 - 解决404问题
+  // 容器日志获取接口
+  app.get('/api/docker/containers/:id/logs', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理获取容器日志请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      const logs = await dockerService.getContainerLogs(req.params.id);
+      res.send(logs);
+    } catch (error) {
+      logger.error(`获取容器日志失败:`, error);
+      res.status(500).json({ error: '获取容器日志失败', details: error.message });
+    }
+  });
+  
+  // 容器详情接口
+  app.get('/api/docker/containers/:id', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理获取容器详情请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      const containerInfo = await dockerService.getContainerStatus(req.params.id);
+      res.json(containerInfo);
+    } catch (error) {
+      logger.error(`获取容器详情失败:`, error);
+      res.status(500).json({ error: '获取容器详情失败', details: error.message });
+    }
+  });
+  
+  // 启动容器接口
+  app.post('/api/docker/containers/:id/start', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理启动容器请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      await dockerService.startContainer(req.params.id);
+      res.json({ success: true, message: '容器启动成功' });
+    } catch (error) {
+      logger.error(`启动容器失败:`, error);
+      res.status(500).json({ error: '启动容器失败', details: error.message });
+    }
+  });
+  
+  // 停止容器接口
+  app.post('/api/docker/containers/:id/stop', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理停止容器请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      await dockerService.stopContainer(req.params.id);
+      res.json({ success: true, message: '容器停止成功' });
+    } catch (error) {
+      logger.error(`停止容器失败:`, error);
+      res.status(500).json({ error: '停止容器失败', details: error.message });
+    }
+  });
+  
+  // 重启容器接口
+  app.post('/api/docker/containers/:id/restart', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理重启容器请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      await dockerService.restartContainer(req.params.id);
+      res.json({ success: true, message: '容器重启成功' });
+    } catch (error) {
+      logger.error(`重启容器失败:`, error);
+      res.status(500).json({ error: '重启容器失败', details: error.message });
+    }
+  });
+  
+  // 更新容器接口
+  app.post('/api/docker/containers/:id/update', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理更新容器请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      const { tag } = req.body;
+      await dockerService.updateContainer(req.params.id, tag);
+      res.json({ success: true, message: '容器更新成功' });
+    } catch (error) {
+      logger.error(`更新容器失败:`, error);
+      res.status(500).json({ error: '更新容器失败', details: error.message });
+    }
+  });
+  
+  // 删除容器接口
+  app.post('/api/docker/containers/:id/remove', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理删除容器请求: ${req.params.id}`);
+      const dockerService = require('./services/dockerService');
+      await dockerService.deleteContainer(req.params.id);
+      res.json({ success: true, message: '容器删除成功' });
+    } catch (error) {
+      logger.error(`删除容器失败:`, error);
+      res.status(500).json({ error: '删除容器失败', details: error.message });
+    }
+  });
+  
+  // 登录接口 (兼容层备份)
+  app.post('/api/login', async (req, res) => {
+    try {
+      const { username, password, captcha } = req.body;
+      
+      if (req.session.captcha !== parseInt(captcha)) {
+        logger.warn(`Captcha verification failed for user: ${username}`);
+        return res.status(401).json({ error: '验证码错误' });
+      }
+
+      const userService = require('./services/userService');
+      const users = await userService.getUsers();
+      const user = users.users.find(u => u.username === username);
+      
+      if (!user) {
+        logger.warn(`User ${username} not found`);
+        return res.status(401).json({ error: '用户名或密码错误' });
+      }
+
+      const bcrypt = require('bcrypt');
+      if (bcrypt.compareSync(password, user.password)) {
+        req.session.user = { username: user.username };
+        
+        // 更新用户登录信息
+        await userService.updateUserLoginInfo(username);
+        
+        logger.info(`User ${username} logged in successfully`);
+        res.json({ success: true });
+      } else {
+        logger.warn(`Login failed for user: ${username}`);
+        res.status(401).json({ error: '用户名或密码错误' });
+      }
+    } catch (error) {
+      logger.error('登录失败:', error);
+      res.status(500).json({ error: '登录处理失败', details: error.message });
+    }
+  });
+  
+  // 修复搜索函数问题 - 完善错误处理
+  app.get('/api/search', async (req, res) => {
+    try {
+      const dockerHubService = require('./services/dockerHubService');
+      const term = req.query.term;
+      
+      if (!term) {
+        return res.status(400).json({ error: '搜索词不能为空' });
+      }
+      
+      // 直接处理搜索,不依赖缓存
+      try {
+        const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${req.query.page || 1}&page_size=25`;
+        const axios = require('axios');
+        const response = await axios.get(url, {
+          timeout: 15000,
+          headers: {
+            'User-Agent': 'DockerHubSearchClient/1.0',
+            'Accept': 'application/json'
+          }
+        });
+        
+        res.json(response.data);
+      } catch (searchError) {
+        logger.error('Docker Hub搜索请求失败:', searchError.message);
+        res.status(500).json({ 
+          error: '搜索Docker Hub失败', 
+          details: searchError.message,
+          retryable: true 
+        });
+      }
+    } catch (error) {
+      logger.error('搜索Docker Hub失败:', error);
+      res.status(500).json({ error: '搜索失败', details: error.message });
+    }
+  });
+  
+  // 获取磁盘空间信息的API
+  app.get('/api/disk-space', requireLogin, async (req, res) => {
+    try {
+      // 使用server-utils中的execCommand函数执行df命令
+      const diskInfo = await execCommand('df -h | grep -E "/$|/home" | head -1');
+      const diskParts = diskInfo.split(/\s+/);
+      
+      if (diskParts.length >= 5) {
+        res.json({
+          diskSpace: `${diskParts[2]}/${diskParts[1]}`, // 已用/总量
+          usagePercent: parseInt(diskParts[4].replace('%', '')) // 使用百分比
+        });
+      } else {
+        throw new Error('磁盘信息格式不正确');
+      }
+    } catch (error) {
+      logger.error('获取磁盘空间信息失败:', error);
+      res.status(500).json({ 
+        error: '获取磁盘空间信息失败', 
+        details: error.message,
+        diskSpace: '未知',
+        usagePercent: 0 
+      });
+    }
+  });
+  
+  // 兼容config API
+  app.get('/api/config', async (req, res) => {
+    try {
+      logger.info('兼容层处理配置请求');
+      const fs = require('fs').promises;
+      const path = require('path');
+      
+      // 配置文件路径
+      const configFilePath = path.join(__dirname, './data/config.json');
+      
+      // 默认配置
+      const DEFAULT_CONFIG = {
+        proxyDomain: 'registry-1.docker.io',
+        logo: '',
+        theme: 'light',
+        menuItems: [
+          {
+            text: "首页",
+            link: "/",
+            newTab: false
+          },
+          {
+            text: "文档",
+            link: "/docs",
+            newTab: false
+          }
+        ]
+      };
+      
+      // 确保配置存在
+      let config = DEFAULT_CONFIG;
+      
+      try {
+        await fs.access(configFilePath);
+        const data = await fs.readFile(configFilePath, 'utf8');
+        config = JSON.parse(data);
+      } catch (err) {
+        // 如果文件不存在或解析失败,使用默认配置
+        logger.warn('读取配置文件失败,将使用默认配置:', err);
+        // 尝试创建配置文件
+        try {
+          await fs.mkdir(path.dirname(configFilePath), { recursive: true });
+          await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
+        } catch (writeErr) {
+          logger.error('创建默认配置文件失败:', writeErr);
+        }
+      }
+      
+      res.json(config);
+    } catch (err) {
+      logger.error('获取配置失败:', err);
+      res.status(500).json({ error: '获取配置失败' });
+    }
+  });
+  
+  // 保存配置API
+  app.post('/api/config', async (req, res) => {
+    try {
+      logger.info('兼容层处理保存配置请求');
+      const fs = require('fs').promises;
+      const path = require('path');
+      
+      const newConfig = req.body;
+      
+      // 验证请求数据
+      if (!newConfig || typeof newConfig !== 'object') {
+        return res.status(400).json({
+          error: '无效的配置数据',
+          details: '配置必须是一个对象'
+        });
+      }
+      
+      const configFilePath = path.join(__dirname, './data/config.json');
+      
+      // 读取现有配置
+      let existingConfig = {};
+      try {
+        const data = await fs.readFile(configFilePath, 'utf8');
+        existingConfig = JSON.parse(data);
+      } catch (err) {
+        // 文件不存在或解析失败时创建目录
+        await fs.mkdir(path.dirname(configFilePath), { recursive: true });
+      }
+      
+      // 合并配置
+      const mergedConfig = { ...existingConfig, ...newConfig };
+      
+      // 保存到文件
+      await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2));
+      
+      res.json({ success: true, message: '配置已保存' });
+    } catch (err) {
+      logger.error('保存配置失败:', err);
+      res.status(500).json({ 
+        error: '保存配置失败',
+        details: err.message 
+      });
+    }
+  });
+  
+  // 文档管理API - 获取文档列表
+  app.get('/api/documents', requireLogin, async (req, res) => {
+    try {
+      logger.info('兼容层处理获取文档列表请求');
+      const docService = require('./services/documentationService');
+      const documents = await docService.getDocumentationList();
+      res.json(documents);
+    } catch (err) {
+      logger.error('获取文档列表失败:', err);
+      res.status(500).json({ error: '获取文档列表失败', details: err.message });
+    }
+  });
+  
+  // 文档管理API - 获取单个文档
+  app.get('/api/documents/:id', async (req, res) => {
+    try {
+      logger.info(`兼容层处理获取文档请求: ${req.params.id}`);
+      const docService = require('./services/documentationService');
+      const document = await docService.getDocument(req.params.id);
+      
+      // 如果文档不是发布状态,只有已登录用户才能访问
+      if (!document.published && !req.session.user) {
+        return res.status(403).json({ error: '没有权限访问该文档' });
+      }
+      
+      res.json(document);
+    } catch (err) {
+      logger.error(`获取文档 ID:${req.params.id} 失败:`, err);
+      if (err.code === 'ENOENT') {
+        return res.status(404).json({ error: '文档不存在' });
+      }
+      res.status(500).json({ error: '获取文档失败', details: err.message });
+    }
+  });
+  
+  // 文档管理API - 保存或更新文档
+  app.put('/api/documents/:id', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理更新文档请求: ${req.params.id}`);
+      const { title, content, published } = req.body;
+      const docService = require('./services/documentationService');
+      
+      // 检查必需参数
+      if (!title) {
+        return res.status(400).json({ error: '文档标题不能为空' });
+      }
+      
+      const docId = req.params.id;
+      await docService.saveDocument(docId, title, content || '', published);
+      
+      res.json({ success: true, id: docId, message: '文档已保存' });
+    } catch (err) {
+      logger.error(`更新文档 ID:${req.params.id} 失败:`, err);
+      res.status(500).json({ error: '保存文档失败', details: err.message });
+    }
+  });
+  
+  // 文档管理API - 创建新文档
+  app.post('/api/documents', requireLogin, async (req, res) => {
+    try {
+      logger.info('兼容层处理创建文档请求');
+      const { title, content, published } = req.body;
+      const docService = require('./services/documentationService');
+      
+      // 检查必需参数
+      if (!title) {
+        return res.status(400).json({ error: '文档标题不能为空' });
+      }
+      
+      // 创建新文档ID (使用时间戳)
+      const docId = Date.now().toString();
+      await docService.saveDocument(docId, title, content || '', published);
+      
+      res.status(201).json({ success: true, id: docId, message: '文档已创建' });
+    } catch (err) {
+      logger.error('创建文档失败:', err);
+      res.status(500).json({ error: '创建文档失败', details: err.message });
+    }
+  });
+  
+  // 文档管理API - 删除文档
+  app.delete('/api/documents/:id', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理删除文档请求: ${req.params.id}`);
+      const docService = require('./services/documentationService');
+      
+      await docService.deleteDocument(req.params.id);
+      res.json({ success: true, message: '文档已删除' });
+    } catch (err) {
+      logger.error(`删除文档 ID:${req.params.id} 失败:`, err);
+      res.status(500).json({ error: '删除文档失败', details: err.message });
+    }
+  });
+  
+  // 文档管理API - 切换文档发布状态
+  app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
+    try {
+      logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`);
+      const docService = require('./services/documentationService');
+      
+      const result = await docService.toggleDocumentPublish(req.params.id);
+      res.json({ 
+        success: true, 
+        published: result.published,
+        message: `文档已${result.published ? '发布' : '取消发布'}`
+      });
+    } catch (err) {
+      logger.error(`切换文档 ID:${req.params.id} 发布状态失败:`, err);
+      res.status(500).json({ error: '切换文档发布状态失败', details: err.message });
+    }
+  });
+  
+  // 网络测试接口
+  app.post('/api/network-test', requireLogin, async (req, res) => {
+    const { type, domain } = req.body;
+    
+    // 验证输入
+    if (!domain || !domain.match(/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) {
+      return res.status(400).json({ error: '无效的域名格式' });
+    }
+    
+    if (!type || !['ping', 'traceroute'].includes(type)) {
+      return res.status(400).json({ error: '无效的测试类型' });
+    }
+    
+    try {
+      const command = type === 'ping' 
+        ? `ping -c 4 ${domain}` 
+        : `traceroute -m 10 ${domain}`;
+        
+      logger.info(`执行网络测试: ${command}`);
+      const result = await execCommand(command, { timeout: 30000 });
+      res.send(result);
+    } catch (error) {
+      logger.error(`执行网络测试命令错误:`, error);
+      
+      if (error.killed) {
+        return res.status(408).send('测试超时');
+      }
+      
+      res.status(500).send('测试执行失败: ' + (error.message || '未知错误'));
+    }
+  });
+  
+  // 用户信息接口
+  app.get('/api/user-info', requireLogin, async (req, res) => {
+    try {
+      const userService = require('./services/userService');
+      const userStats = await userService.getUserStats(req.session.user.username);
+      
+      res.json(userStats);
+    } catch (error) {
+      logger.error('获取用户信息失败:', error);
+      res.status(500).json({ error: '获取用户信息失败', details: error.message });
+    }
+  });
+  
+  // 修改密码接口
+  app.post('/api/change-password', requireLogin, async (req, res) => {
+      const { currentPassword, newPassword } = req.body;
+      const username = req.session.user.username;
+      
+      if (!currentPassword || !newPassword) {
+          return res.status(400).json({ error: '当前密码和新密码不能为空' });
+      }
+      
+      try {
+          const userService = require('./services/userService');
+          await userService.changePassword(username, currentPassword, newPassword);
+          res.json({ success: true, message: '密码修改成功' });
+      } catch (error) {
+          logger.error(`用户 ${username} 修改密码失败:`, error);
+          res.status(400).json({ error: error.message || '修改密码失败' }); // 返回具体的错误信息
+      }
+  });
+  
+  // 系统资源兼容路由
+  app.get('/api/system-resources', requireLogin, async (req, res) => {
+    try {
+      const startTime = Date.now();
+      logger.info('兼容层: 请求 /api/system-resources');
+      
+      // 获取CPU信息
+      const cpuCores = os.cpus().length;
+      const cpuModel = os.cpus()[0].model;
+      const cpuSpeed = os.cpus()[0].speed;
+      const loadAvg = os.loadavg();
+      
+      // 获取内存信息
+      const totalMem = os.totalmem();
+      const freeMem = os.freemem();
+      const usedMem = totalMem - freeMem;
+      const memoryPercent = ((usedMem / totalMem) * 100).toFixed(1) + '%';
+      
+      // 获取磁盘信息
+      let diskCommand = '';
+      if (process.platform === 'win32') {
+        diskCommand = 'wmic logicaldisk get size,freespace,caption';
+      } else {
+        // 在 macOS 和 Linux 上使用 df 命令
+        diskCommand = 'df -h /';
+      }
+      
+      try {
+        // 执行磁盘命令
+        logger.debug(`执行磁盘命令: ${diskCommand}`);
+        const { stdout } = await execPromise(diskCommand, { timeout: 5000 });
+        logger.debug(`磁盘命令输出: ${stdout}`);
+        
+        // 解析磁盘信息
+        let disk = { size: "未知", used: "未知", available: "未知", percent: "未知" };
+        
+        if (process.platform === 'win32') {
+          // Windows解析逻辑不变
+          // ... (省略Windows解析代码)
+        } else {
+          // macOS/Linux格式解析
+          const lines = stdout.trim().split('\n');
+          if (lines.length >= 2) {
+            const headerParts = lines[0].trim().split(/\s+/);
+            const dataParts = lines[1].trim().split(/\s+/);
+            
+            logger.debug(`解析磁盘信息, 头部: ${headerParts}, 数据: ${dataParts}`);
+            
+            // 检查MacOS格式 (通常是Filesystem Size Used Avail Capacity iused ifree %iused Mounted on)
+            const isMacOS = headerParts.includes('Capacity') && headerParts.includes('iused');
+            
+            if (isMacOS) {
+              // macOS格式处理
+              const fsIndex = 0; // Filesystem
+              const sizeIndex = 1; // Size
+              const usedIndex = 2; // Used
+              const availIndex = 3; // Avail
+              const percentIndex = 4; // Capacity
+              const mountedIndex = headerParts.indexOf('Mounted') + 1; // Mounted on
+              
+              disk = {
+                filesystem: dataParts[fsIndex],
+                size: dataParts[sizeIndex],
+                used: dataParts[usedIndex],
+                available: dataParts[availIndex],
+                percent: dataParts[percentIndex],
+                mountedOn: dataParts[mountedIndex] || '/'
+              };
+            } else {
+              // 标准Linux格式处理 (通常是Filesystem Size Used Avail Use% Mounted on)
+              const fsIndex = 0; // Filesystem
+              const sizeIndex = 1; // Size
+              const usedIndex = 2; // Used
+              const availIndex = 3; // Avail
+              const percentIndex = 4; // Use%
+              const mountedIndex = 5; // Mounted on
+              
+              disk = {
+                filesystem: dataParts[fsIndex],
+                size: dataParts[sizeIndex],
+                used: dataParts[usedIndex],
+                available: dataParts[availIndex],
+                percent: dataParts[percentIndex],
+                mountedOn: dataParts[mountedIndex] || '/'
+              };
+            }
+          }
+        }
+        
+        // 构建最终结果
+        const result = {
+          cpu: {
+            cores: cpuCores,
+            model: cpuModel,
+            speed: cpuSpeed,
+            loadAvg: loadAvg
+          },
+          memory: {
+            total: totalMem,
+            free: freeMem,
+            used: usedMem,
+            percent: memoryPercent
+          },
+          disk: disk,
+          uptime: os.uptime()
+        };
+        
+        logger.debug(`系统资源API返回结果: ${JSON.stringify(result)}`);
+        
+        // 计算处理时间并返回结果
+        const endTime = Date.now();
+        logger.info(`兼容层: /api/system-resources 请求完成,耗时 ${endTime - startTime}ms`);
+        res.json(result);
+      } catch (diskError) {
+        // 磁盘信息获取失败时,仍然返回CPU和内存信息
+        logger.error(`获取磁盘信息失败: ${diskError.message}`);
+        
+        const result = {
+          cpu: {
+            cores: cpuCores,
+            model: cpuModel,
+            speed: cpuSpeed,
+            loadAvg: loadAvg
+          },
+          memory: {
+            total: totalMem,
+            free: freeMem,
+            used: usedMem,
+            percent: memoryPercent
+          },
+          disk: { size: "未知", used: "未知", available: "未知", percent: "未知" },
+          uptime: os.uptime(),
+          diskError: diskError.message
+        };
+        
+        // 计算处理时间并返回结果(即使有错误)
+        const endTime = Date.now();
+        logger.info(`兼容层: /api/system-resources 请求完成(但磁盘信息失败),耗时 ${endTime - startTime}ms`);
+        res.json(result);
+      }
+    } catch (error) {
+      logger.error(`系统资源API错误: ${error.message}`);
+      res.status(500).json({ error: '获取系统资源信息失败', message: error.message });
+    }
+  });
+  
+  // 登出接口
+  app.post('/api/logout', (req, res) => {
+    if (req.session) {
+      req.session.destroy(err => {
+        if (err) {
+          logger.error('销毁会话失败:', err);
+          return res.status(500).json({ error: '退出登录失败' });
+        }
+        // 清除客户端的 connect.sid cookie
+        res.clearCookie('connect.sid', { path: '/' }); // 确保路径与设置时一致
+        logger.info('用户已成功登出');
+        res.json({ success: true, message: '已成功登出' });
+      });
+    } else {
+      // 如果没有会话,也认为登出成功
+      logger.info('用户已登出(无会话)');
+      res.json({ success: true, message: '已成功登出' });
+    }
+  });
+  
+  logger.success('API兼容层加载完成');
+};

+ 54 - 0
hubcmdui/config.js

@@ -0,0 +1,54 @@
+/**
+ * 应用全局配置文件
+ */
+
+// 环境变量
+const ENV = process.env.NODE_ENV || 'development';
+
+// 应用配置
+const config = {
+  // 通用配置
+  common: {
+    port: process.env.PORT || 3000,
+    sessionSecret: process.env.SESSION_SECRET || 'OhTq3faqSKoxbV%NJV',
+    logLevel: process.env.LOG_LEVEL || 'info'
+  },
+  
+  // 开发环境配置
+  development: {
+    debug: true,
+    cors: {
+      origin: '*',
+      credentials: true
+    },
+    secureSession: false
+  },
+  
+  // 生产环境配置
+  production: {
+    debug: false,
+    cors: {
+      origin: 'https://yourdomain.com',
+      credentials: true
+    },
+    secureSession: true
+  },
+  
+  // 测试环境配置
+  test: {
+    debug: true,
+    cors: {
+      origin: '*',
+      credentials: true
+    },
+    secureSession: false,
+    port: 3001
+  }
+};
+
+// 导出合并后的配置
+module.exports = {
+  ...config.common,
+  ...config[ENV],
+  env: ENV
+};

+ 20 - 23
hubcmdui/config.json

@@ -1,39 +1,36 @@
 {
-  "logo": "",
+  "theme": "light",
+  "language": "zh_CN",
+  "notifications": true,
+  "autoRefresh": true,
+  "refreshInterval": 30000,
+  "dockerHost": "localhost",
+  "dockerPort": 2375,
+  "useHttps": false,
   "menuItems": [
     {
-      "text": "首页",
-      "link": "",
+      "text": "控制台",
+      "link": "/admin",
       "newTab": false
     },
     {
-      "text": "GitHub",
-      "link": "https://github.com/dqzboy/Docker-Proxy",
-      "newTab": true
-    },
-    {
-      "text": "VPS推荐",
-      "link": "https://dqzboy.github.io/proxyui/racknerd",
-      "newTab": true
-    }
-  ],
-  "adImages": [
-    {
-      "url": "https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/reacknerd-ad.png",
-      "link": "https://my.racknerd.com/aff.php?aff=12151"
+      "text": "镜像搜索",
+      "link": "/",
+      "newTab": false
     },
     {
-      "url": "https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/racknerd_vps.png",
-      "link": "https://my.racknerd.com/aff.php?aff=12151"
+      "text": "文档",
+      "link": "/docs",
+      "newTab": false
     },
     {
-      "url": "https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy-vip.png",
-      "link": "https://www.dqzboy.com/17834.html"
+      "text": "GitHub",
+      "link": "https://github.com/dqzboy/hubcmdui",
+      "newTab": true
     }
   ],
-  "proxyDomain": "dqzboy.github.io",
   "monitoringConfig": {
-    "notificationType": "telegram",
+    "notificationType": "wechat",
     "webhookUrl": "",
     "telegramToken": "",
     "telegramChatId": "",

+ 1 - 0
hubcmdui/config/menu.json

@@ -0,0 +1 @@
+[]

+ 8 - 0
hubcmdui/config/monitoring.json

@@ -0,0 +1,8 @@
+{
+  "isEnabled": false,
+  "notificationType": "wechat",
+  "webhookUrl": "",
+  "telegramToken": "",
+  "telegramChatId": "",
+  "monitorInterval": 60
+}

+ 29 - 0
hubcmdui/data/config.json

@@ -0,0 +1,29 @@
+{
+  "logo": "",
+  "menuItems": [
+    {
+      "text": "首页",
+      "link": "",
+      "newTab": false
+    },
+    {
+      "text": "GitHub",
+      "link": "https://github.com/dqzboy/Docker-Proxy",
+      "newTab": true
+    },
+    {
+      "text": "VPS推荐",
+      "link": "https://dqzboy.github.io/proxyui/racknerd",
+      "newTab": true
+    }
+  ],
+  "monitoringConfig": {
+    "notificationType": "telegram",
+    "webhookUrl": "",
+    "telegramToken": "",
+    "telegramChatId": "",
+    "monitorInterval": 60,
+    "isEnabled": false
+  },
+  "proxyDomain": "dqzboy.github.io"
+}

+ 11 - 1
hubcmdui/docker-compose.yaml

@@ -7,4 +7,14 @@ services:
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock
     ports:
-      - 30080:3000
+      - 30080:3000
+    environment:
+      # 日志配置
+      - LOG_LEVEL=INFO # 可选: TRACE, DEBUG, INFO, SUCCESS, WARN, ERROR, FATAL
+      - SIMPLE_LOGS=true # 启用简化日志输出,减少冗余信息
+      # - DETAILED_LOGS=false # 默认关闭详细日志记录(请求体、查询参数等)
+      # - SHOW_STACK=false # 默认关闭错误堆栈跟踪
+      # - LOG_FILE_ENABLED=true # 是否启用文件日志,默认启用
+      # - LOG_CONSOLE_ENABLED=true # 是否启用控制台日志,默认启用
+      # - LOG_MAX_SIZE=10 # 单个日志文件最大大小(MB),默认10MB
+      # - LOG_MAX_FILES=14 # 保留的日志文件数量,默认14个

BIN
hubcmdui/documentation/.DS_Store


+ 0 - 1
hubcmdui/documentation/1724594777670.json

@@ -1 +0,0 @@
-{"title":"Docker 配置镜像加速","content":"### Docker 配置镜像加速\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```shell\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n  \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```","published":true}

+ 0 - 1
hubcmdui/documentation/1737713570870.json

@@ -1 +0,0 @@
-{"title":"Containerd 配置镜像加速","content":"### Containerd 配置镜像加速\n-  `/etc/containerd/config.toml`,添加如下的配置:\n\n```yaml\n    [plugins.\"io.containerd.grpc.v1.cri\".registry]\n      [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n```","published":true}

+ 0 - 1
hubcmdui/documentation/1737713707391.json

@@ -1 +0,0 @@
-{"title":"Podman 配置镜像加速","content":"### Podman 配置镜像加速\n- 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```yaml\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```","published":true}

+ 7 - 0
hubcmdui/documentation/1743542841590.json

@@ -0,0 +1,7 @@
+{
+  "title": "Docker 配置镜像加速",
+  "content": "# Docker 配置镜像加速\n\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n  \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```",
+  "published": true,
+  "createdAt": "2025-04-01T21:27:21.591Z",
+  "updatedAt": "2025-04-01T21:35:20.004Z"
+}

+ 7 - 0
hubcmdui/documentation/1743543376091.json

@@ -0,0 +1,7 @@
+{
+  "title": "Containerd 配置镜像加速",
+  "content": "# Containerd 配置镜像加速\n\n\n* `/etc/containerd/config.toml`,添加如下的配置:\n\n```bash\n    [plugins.\"io.containerd.grpc.v1.cri\".registry]\n      [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n        [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n          endpoint = [\"https://<代理加速地址>\"]\n```",
+  "published": true,
+  "createdAt": "2025-04-01T21:36:16.092Z",
+  "updatedAt": "2025-04-01T21:36:18.103Z"
+}

+ 7 - 0
hubcmdui/documentation/1743543400369.json

@@ -0,0 +1,7 @@
+{
+  "title": "Podman 配置镜像加速",
+  "content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```",
+  "published": true,
+  "createdAt": "2025-04-01T21:36:40.369Z",
+  "updatedAt": "2025-04-01T21:36:41.977Z"
+}

+ 83 - 0
hubcmdui/download-images.js

@@ -0,0 +1,83 @@
+/**
+ * 下载必要的图片资源
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const https = require('https');
+const logger = require('./logger');
+const { ensureDirectoriesExist } = require('./init-dirs');
+
+// 背景图片URL
+const LOGIN_BG_URL = 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=1470&auto=format&fit=crop';
+
+// 下载图片函数
+function downloadImage(url, dest) {
+  return new Promise((resolve, reject) => {
+    const file = fs.createWriteStream(dest);
+    
+    https.get(url, response => {
+      if (response.statusCode !== 200) {
+        reject(new Error(`Failed to download image. Status code: ${response.statusCode}`));
+        return;
+      }
+      
+      response.pipe(file);
+      
+      file.on('finish', () => {
+        file.close();
+        logger.success(`Image downloaded to: ${dest}`);
+        resolve();
+      });
+      
+      file.on('error', err => {
+        fs.unlink(dest).catch(() => {}); // 删除文件(如果存在)
+        reject(err);
+      });
+    }).on('error', err => {
+      fs.unlink(dest).catch(() => {}); // 删除文件(如果存在)
+      reject(err);
+    });
+  });
+}
+
+// 主函数
+async function downloadImages() {
+  try {
+    // 确保目录存在
+    await ensureDirectoriesExist();
+    
+    // 下载登录背景图片
+    const loginBgPath = path.join(__dirname, 'web', 'images', 'login-bg.jpg');
+    try {
+      await fs.access(loginBgPath);
+      logger.info('Login background image already exists, skipping download');
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        logger.info('Downloading login background image...');
+        try {
+          // 确保images目录存在
+          await fs.mkdir(path.dirname(loginBgPath), { recursive: true });
+          await downloadImage(LOGIN_BG_URL, loginBgPath);
+        } catch (downloadError) {
+          logger.error(`Download error: ${downloadError.message}`);
+          // 下载失败时使用备用解决方案
+          await fs.writeFile(loginBgPath, 'Failed to download', 'utf8');
+          logger.warn('Created placeholder image file');
+        }
+      } else {
+        throw error;
+      }
+    }
+    
+    logger.success('All images downloaded successfully');
+  } catch (error) {
+    logger.error('Error downloading images:', error);
+  }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+  downloadImages();
+}
+
+module.exports = { downloadImages };

+ 86 - 0
hubcmdui/init-dirs.js

@@ -0,0 +1,86 @@
+/**
+ * 目录初始化模块 - 确保应用需要的所有目录都存在
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('./logger');
+
+/**
+ * 确保所有必需的目录存在
+ */
+// 添加缓存机制
+const checkedDirs = new Set();
+
+async function ensureDirectoriesExist() {
+  const dirs = [
+    // 文档目录
+    path.join(__dirname, 'documentation'),
+    // 日志目录
+    path.join(__dirname, 'logs'),
+    // 图片目录
+    path.join(__dirname, 'web', 'images'),
+    // 数据目录
+    path.join(__dirname, 'data'),
+    // 配置目录
+    path.join(__dirname, 'config'),
+    // 临时文件目录
+    path.join(__dirname, 'temp'),
+    // session 目录
+    path.join(__dirname, 'data', 'sessions'),
+    // 文档数据目录
+    path.join(__dirname, 'web', 'data', 'documentation')
+  ];
+  
+  for (const dir of dirs) {
+    if (checkedDirs.has(dir)) continue;
+    
+    try {
+      await fs.access(dir);
+      logger.info(`目录已存在: ${dir}`);
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        try {
+          await fs.mkdir(dir, { recursive: true });
+          logger.success(`创建目录: ${dir}`);
+        } catch (mkdirError) {
+          logger.error(`创建目录 ${dir} 失败: ${mkdirError.message}`);
+          throw mkdirError;
+        }
+      } else {
+        logger.error(`检查目录 ${dir} 失败: ${error.message}`);
+        throw error;
+      }
+    }
+    
+    checkedDirs.add(dir);
+  }
+  
+  // 确保文档索引存在,但不再添加默认文档
+  const docIndexPath = path.join(__dirname, 'web', 'data', 'documentation', 'index.json');
+  try {
+    await fs.access(docIndexPath);
+    logger.info('文档索引已存在');
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      try {
+        // 创建一个空的文档索引
+        await fs.writeFile(docIndexPath, JSON.stringify([]), 'utf8');
+        logger.success('创建了空的文档索引文件');
+      } catch (writeError) {
+        logger.error(`创建文档索引失败: ${writeError.message}`);
+      }
+    }
+  }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+  ensureDirectoriesExist()
+    .then(() => logger.info('目录初始化完成'))
+    .catch(err => {
+      logger.error('目录初始化失败:', err);
+      process.exit(1);
+    });
+}
+
+module.exports = { ensureDirectoriesExist };

+ 340 - 182
hubcmdui/logger.js

@@ -1,216 +1,374 @@
-const fs = require('fs').promises;
+const fs = require('fs');
+const fsPromises = fs.promises;
 const path = require('path');
 const util = require('util');
+const os = require('os');
 
-// 日志级别配置
+// 日志级别定义
 const LOG_LEVELS = {
-    debug: 0,
-    info: 1,
-    success: 2,
-    warn: 3,
-    error: 4,
-    fatal: 5
+  TRACE: { priority: 0, color: 'grey', prefix: 'TRACE' },
+  DEBUG: { priority: 1, color: 'blue', prefix: 'DEBUG' },
+  INFO: { priority: 2, color: 'green', prefix: 'INFO' },
+  SUCCESS: { priority: 3, color: 'greenBright', prefix: 'SUCCESS' },
+  WARN: { priority: 4, color: 'yellow', prefix: 'WARN' },
+  ERROR: { priority: 5, color: 'red', prefix: 'ERROR' },
+  FATAL: { priority: 6, color: 'redBright', prefix: 'FATAL' }
 };
 
-// 默认配置
-const config = {
-    level: process.env.LOG_LEVEL || 'info',
-    logToFile: process.env.LOG_TO_FILE === 'true',
-    logDirectory: path.join(__dirname, 'logs'),
-    logFileName: 'app.log',
-    maxLogSize: 10 * 1024 * 1024, // 10MB
-    colorize: process.env.NODE_ENV !== 'production'
+// 彩色日志实现
+const colors = {
+  grey: text => `\x1b[90m${text}\x1b[0m`,
+  blue: text => `\x1b[34m${text}\x1b[0m`,
+  green: text => `\x1b[32m${text}\x1b[0m`,
+  greenBright: text => `\x1b[92m${text}\x1b[0m`,
+  yellow: text => `\x1b[33m${text}\x1b[0m`,
+  red: text => `\x1b[31m${text}\x1b[0m`,
+  redBright: text => `\x1b[91m${text}\x1b[0m`
 };
 
+// 日志配置
+const LOG_CONFIG = {
+  // 默认日志级别
+  level: process.env.LOG_LEVEL || 'INFO',
+  // 日志文件配置
+  file: {
+    enabled: true,
+    dir: path.join(__dirname, 'logs'),
+    nameFormat: 'app-%DATE%.log',
+    maxSize: 10 * 1024 * 1024, // 10MB
+    maxFiles: 14, // 保留14天的日志
+  },
+  // 控制台输出配置
+  console: {
+    enabled: true,
+    colorize: true,
+    // 简化输出在控制台
+    simplified: process.env.NODE_ENV === 'production' || process.env.SIMPLE_LOGS === 'true'
+  },
+  // 是否打印请求体、查询参数等详细信息(默认关闭)
+  includeDetails: process.env.NODE_ENV === 'development' || process.env.DETAILED_LOGS === 'true',
+  // 是否显示堆栈跟踪(默认关闭)
+  includeStack: process.env.NODE_ENV === 'development' || process.env.SHOW_STACK === 'true'
+};
+
+// 根据环境变量初始化配置
+function initConfig() {
+  // 检查环境变量并更新配置
+  if (process.env.LOG_FILE_ENABLED === 'false') {
+    LOG_CONFIG.file.enabled = false;
+  }
+  
+  if (process.env.LOG_CONSOLE_ENABLED === 'false') {
+    LOG_CONFIG.console.enabled = false;
+  }
+  
+  if (process.env.LOG_MAX_SIZE) {
+    LOG_CONFIG.file.maxSize = parseInt(process.env.LOG_MAX_SIZE) * 1024 * 1024;
+  }
+  
+  if (process.env.LOG_MAX_FILES) {
+    LOG_CONFIG.file.maxFiles = parseInt(process.env.LOG_MAX_FILES);
+  }
+  
+  if (process.env.DETAILED_LOGS === 'true') {
+    LOG_CONFIG.includeDetails = true;
+  } else if (process.env.DETAILED_LOGS === 'false') {
+    LOG_CONFIG.includeDetails = false;
+  }
+  
+  if (process.env.SIMPLE_LOGS === 'true') {
+    LOG_CONFIG.console.simplified = true;
+  } else if (process.env.SIMPLE_LOGS === 'false') {
+    LOG_CONFIG.console.simplified = false;
+  }
+  
+  // 验证日志级别是否有效
+  if (!LOG_LEVELS[LOG_CONFIG.level]) {
+    console.warn(`无效的日志级别: ${LOG_CONFIG.level},将使用默认级别: INFO`);
+    LOG_CONFIG.level = 'INFO';
+  }
+}
+
+// 初始化配置
+initConfig();
+
 // 确保日志目录存在
-async function ensureLogDirectory() {
-    if (config.logToFile) {
-        try {
-            await fs.access(config.logDirectory);
-        } catch (error) {
-            if (error.code === 'ENOENT') {
-                await fs.mkdir(config.logDirectory, { recursive: true });
-                console.log(`Created log directory: ${config.logDirectory}`);
-            } else {
-                throw error;
-            }
-        }
+async function ensureLogDir() {
+  if (!LOG_CONFIG.file.enabled) return;
+  
+  try {
+    await fsPromises.access(LOG_CONFIG.file.dir);
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      await fsPromises.mkdir(LOG_CONFIG.file.dir, { recursive: true });
+    } else {
+      console.error('无法创建日志目录:', error);
     }
+  }
 }
 
-// 格式化时间 - 改进为更易读的格式
-function getTimestamp() {
-    const now = new Date();
-    const year = now.getFullYear();
-    const month = String(now.getMonth() + 1).padStart(2, '0');
-    const day = String(now.getDate()).padStart(2, '0');
-    const hours = String(now.getHours()).padStart(2, '0');
-    const minutes = String(now.getMinutes()).padStart(2, '0');
-    const seconds = String(now.getSeconds()).padStart(2, '0');
-    const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
-    
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
+// 生成当前日志文件名
+function getCurrentLogFile() {
+  const today = new Date().toISOString().split('T')[0];
+  return path.join(LOG_CONFIG.file.dir, LOG_CONFIG.file.nameFormat.replace(/%DATE%/g, today));
 }
 
-// 颜色代码
-const COLORS = {
-    reset: '\x1b[0m',
-    bright: '\x1b[1m',
-    dim: '\x1b[2m',
-    underscore: '\x1b[4m',
-    blink: '\x1b[5m',
-    reverse: '\x1b[7m',
-    hidden: '\x1b[8m',
-    
-    // 前景色
-    black: '\x1b[30m',
-    red: '\x1b[31m',
-    green: '\x1b[32m',
-    yellow: '\x1b[33m',
-    blue: '\x1b[34m',
-    magenta: '\x1b[35m',
-    cyan: '\x1b[36m',
-    white: '\x1b[37m',
-    
-    // 背景色
-    bgBlack: '\x1b[40m',
-    bgRed: '\x1b[41m',
-    bgGreen: '\x1b[42m',
-    bgYellow: '\x1b[43m',
-    bgBlue: '\x1b[44m',
-    bgMagenta: '\x1b[45m',
-    bgCyan: '\x1b[46m',
-    bgWhite: '\x1b[47m'
-};
-
-// 日志级别对应的颜色和标签
-const LEVEL_STYLES = {
-    debug: { color: COLORS.cyan, label: 'DEBUG' },
-    info: { color: COLORS.blue, label: 'INFO ' },
-    success: { color: COLORS.green, label: 'DONE ' },
-    warn: { color: COLORS.yellow, label: 'WARN ' },
-    error: { color: COLORS.red, label: 'ERROR' },
-    fatal: { color: COLORS.bright + COLORS.red, label: 'FATAL' }
-};
+// 检查是否需要轮转日志
+async function checkRotation() {
+  if (!LOG_CONFIG.file.enabled) return false;
+  
+  const currentLogFile = getCurrentLogFile();
+  try {
+    const stats = await fsPromises.stat(currentLogFile);
+    if (stats.size >= LOG_CONFIG.file.maxSize) {
+      return true;
+    }
+  } catch (err) {
+    // 文件不存在,不需要轮转
+    if (err.code !== 'ENOENT') {
+      console.error('检查日志文件大小失败:', err);
+    }
+  }
+  return false;
+}
 
-// 创建日志条目 - 改进格式
-function createLogEntry(level, message, meta = {}) {
-    const timestamp = getTimestamp();
-    const levelInfo = LEVEL_STYLES[level] || { label: level.toUpperCase() };
+// 轮转日志文件
+async function rotateLogFile() {
+  if (!LOG_CONFIG.file.enabled) return;
+  
+  const currentLogFile = getCurrentLogFile();
+  try {
+    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+    const rotatedFile = `${currentLogFile}.${timestamp}`;
     
-    // 元数据格式化 - 更简洁的呈现方式
-    let metaOutput = '';
-    if (meta instanceof Error) {
-        metaOutput = `\n  ${COLORS.red}Error: ${meta.message}${COLORS.reset}`;
-        if (meta.stack) {
-            metaOutput += `\n  ${COLORS.dim}Stack: ${meta.stack.split('\n').join('\n  ')}${COLORS.reset}`;
-        }
-    } else if (Object.keys(meta).length > 0) {
-        // 检查是否为HTTP请求信息,如果是则使用更简洁的格式
-        if (meta.ip && meta.userAgent) {
-            metaOutput = ` ${COLORS.dim}from ${meta.ip}${COLORS.reset}`;
-        } else {
-            // 对于其他元数据,仍然使用检查器但格式更友好
-            metaOutput = `\n  ${util.inspect(meta, { colors: true, depth: 3 })}`;
-        }
+    try {
+      // 检查文件是否存在
+      await fsPromises.access(currentLogFile);
+      // 重命名文件
+      await fsPromises.rename(currentLogFile, rotatedFile);
+      
+      // 清理旧日志文件
+      await cleanupOldLogFiles();
+    } catch (err) {
+      // 如果文件不存在,则忽略
+      if (err.code !== 'ENOENT') {
+        console.error('轮转日志文件失败:', err);
+      }
     }
+  } catch (err) {
+    console.error('轮转日志文件失败:', err);
+  }
+}
+
+// 清理旧日志文件
+async function cleanupOldLogFiles() {
+  if (!LOG_CONFIG.file.enabled || LOG_CONFIG.file.maxFiles <= 0) return;
+  
+  try {
+    const files = await fsPromises.readdir(LOG_CONFIG.file.dir);
+    const logFilePattern = LOG_CONFIG.file.nameFormat.replace(/%DATE%/g, '\\d{4}-\\d{2}-\\d{2}');
+    const logFileRegex = new RegExp(`^${logFilePattern}(\\.[\\d-T]+)?$`);
     
-    // 为控制台格式化日志
-    const consoleOutput = config.colorize ? 
-        `${COLORS.dim}${timestamp}${COLORS.reset} [${levelInfo.color}${levelInfo.label}${COLORS.reset}] ${message}${metaOutput}` :
-        `${timestamp} [${levelInfo.label}] ${message}${metaOutput ? ' ' + metaOutput.trim() : ''}`;
+    const logFiles = files
+      .filter(file => logFileRegex.test(file))
+      .map(file => ({
+        name: file,
+        path: path.join(LOG_CONFIG.file.dir, file),
+        time: fs.statSync(path.join(LOG_CONFIG.file.dir, file)).mtime.getTime()
+      }))
+      .sort((a, b) => b.time - a.time); // 按修改时间降序排序
     
-    // 为文件准备JSON格式日志
-    const logObject = {
-        timestamp,
-        level: level,
-        message
-    };
+    // 保留最新的maxFiles个文件,删除其余的
+    const filesToDelete = logFiles.slice(LOG_CONFIG.file.maxFiles);
+    for (const file of filesToDelete) {
+      try {
+        await fsPromises.unlink(file.path);
+      } catch (err) {
+        console.error(`删除旧日志文件 ${file.path} 失败:`, err);
+      }
+    }
+  } catch (err) {
+    console.error('清理旧日志文件失败:', err);
+  }
+}
+
+// 写入日志文件
+async function writeToLogFile(message) {
+  if (!LOG_CONFIG.file.enabled) return;
+  
+  try {
+    await ensureLogDir();
     
-    if (Object.keys(meta).length > 0) {
-        logObject.meta = meta instanceof Error ? 
-            { name: meta.name, message: meta.message, stack: meta.stack } : 
-            meta;
+    // 检查是否需要轮转日志
+    if (await checkRotation()) {
+      await rotateLogFile();
     }
     
-    return {
-        formatted: consoleOutput,
-        json: JSON.stringify(logObject)
-    };
+    const currentLogFile = getCurrentLogFile();
+    const logEntry = `${message}\n`;
+    await fsPromises.appendFile(currentLogFile, logEntry);
+  } catch (error) {
+    console.error('写入日志文件失败:', error);
+  }
 }
 
-// 日志函数
-async function log(level, message, meta = {}) {
-    if (LOG_LEVELS[level] < LOG_LEVELS[config.level]) {
-        return;
+// 格式化日志消息
+function formatLogMessage(level, message, details) {
+  const timestamp = new Date().toISOString();
+  const prefix = `[${level.prefix}]`;
+  
+  // 简化标准日志格式:时间戳 [日志级别] 消息
+  const standardMessage = `${timestamp} ${prefix} ${message}`;
+  
+  let detailsStr = '';
+  
+  if (details) {
+    if (details instanceof Error) {
+      detailsStr = ` ${details.message}`;
+      if (LOG_CONFIG.includeStack && details.stack) {
+        detailsStr += `\n${details.stack}`;
+      }
+    } else if (typeof details === 'object') {
+      try {
+        // 只输出关键字段
+        const filteredDetails = { ...details };
+        // 移除大型或不重要的字段
+        ['stack', 'userAgent', 'referer'].forEach(key => {
+          if (key in filteredDetails) delete filteredDetails[key];
+        });
+        
+        // 使用紧凑格式输出JSON
+        detailsStr = Object.keys(filteredDetails).length > 0 
+          ? ` ${JSON.stringify(filteredDetails)}` 
+          : '';
+      } catch (e) {
+        detailsStr = ` ${util.inspect(details, { depth: 1, colors: false, compact: true })}`;
+      }
+    } else {
+      detailsStr = ` ${details}`;
     }
+  }
+  
+  return {
+    console: LOG_CONFIG.console.colorize 
+      ? `${timestamp} ${colors[level.color](prefix)} ${message}${detailsStr}`
+      : `${timestamp} ${prefix} ${message}${detailsStr}`,
+    file: `${standardMessage}${detailsStr}`
+  };
+}
 
-    const { formatted, json } = createLogEntry(level, message, meta);
-
-    // 控制台输出
-    console.log(formatted);
+// 检查当前日志级别是否应该记录指定级别的日志
+function shouldLog(levelName) {
+  const configLevel = LOG_LEVELS[LOG_CONFIG.level];
+  const messageLevel = LOG_LEVELS[levelName];
+  
+  if (!configLevel || !messageLevel) {
+    return true; // 默认允许记录
+  }
+  
+  return messageLevel.priority >= configLevel.priority;
+}
 
-    // 文件日志
-    if (config.logToFile) {
-        try {
-            await ensureLogDirectory();
-            const logFilePath = path.join(config.logDirectory, config.logFileName);
-            await fs.appendFile(logFilePath, json + '\n', 'utf8');
-        } catch (err) {
-            console.error(`${COLORS.red}Error writing to log file: ${err.message}${COLORS.reset}`);
-        }
-    }
+// 记录日志的通用函数
+function log(level, message, details) {
+  if (!LOG_LEVELS[level]) {
+    level = 'INFO';
+  }
+  
+  // 检查是否应该记录该级别的日志
+  if (!shouldLog(level)) {
+    return;
+  }
+  
+  const formattedMessage = formatLogMessage(LOG_LEVELS[level], message, details);
+  
+  // 控制台输出
+  if (LOG_CONFIG.console.enabled) {
+    console.log(formattedMessage.console);
+  }
+  
+  // 写入文件
+  if (LOG_CONFIG.file.enabled) {
+    writeToLogFile(formattedMessage.file);
+  }
 }
 
-// 日志API
-const logger = {
-    debug: (message, meta = {}) => log('debug', message, meta),
-    info: (message, meta = {}) => log('info', message, meta),
-    success: (message, meta = {}) => log('success', message, meta),
-    warn: (message, meta = {}) => log('warn', message, meta),
-    error: (message, meta = {}) => log('error', message, meta),
-    fatal: (message, meta = {}) => log('fatal', message, meta),
+// 请求日志函数
+function request(req, res, duration) {
+  const method = req.method;
+  const url = req.originalUrl || req.url;
+  const status = res.statusCode;
+  const ip = req.ip ? req.ip.replace(/::ffff:/, '') : 'unknown';
+  
+  // 根据状态码确定日志级别
+  let level = 'INFO';
+  if (status >= 400 && status < 500) level = 'WARN';
+  if (status >= 500) level = 'ERROR';
+  
+  // 简化日志消息格式
+  const logMessage = `${method} ${url} ${status} ${duration}ms`;
+  
+  // 只有在需要时才收集详细信息
+  let details = null;
+  
+  // 如果请求标记为跳过详细日志或不是开发环境,则不记录详细信息
+  if (!req.skipDetailedLogging && LOG_CONFIG.includeDetails) {
+    // 记录最少的必要信息
+    details = {};
     
-    // 配置方法
-    configure: (options) => {
-        Object.assign(config, options);
-    },
+    // 只在错误状态码时记录更多信息
+    if (status >= 400) {
+      // 安全地记录请求参数,过滤敏感信息
+      const sanitizedBody = req.sanitizedBody || req.body;
+      if (sanitizedBody && Object.keys(sanitizedBody).length > 0) {
+        // 屏蔽敏感字段
+        const filtered = { ...sanitizedBody };
+        ['password', 'token', 'apiKey', 'secret', 'credentials'].forEach(key => {
+          if (key in filtered) filtered[key] = '******';
+        });
+        details.body = filtered;
+      }
+      
+      if (req.params && Object.keys(req.params).length > 0) {
+        details.params = req.params;
+      }
+      
+      if (req.query && Object.keys(req.query).length > 0) {
+        details.query = req.query;
+      }
+    }
     
-    // HTTP请求日志方法 - 简化输出格式
-    request: (req, res, duration) => {
-        const status = res.statusCode;
-        const method = req.method;
-        const url = req.originalUrl || req.url;
-        const userAgent = req.headers['user-agent'] || '-';
-        const ip = req.ip || req.connection.remoteAddress || '-';
-        
-        let level = 'info';
-        if (status >= 500) level = 'error';
-        else if (status >= 400) level = 'warn';
-        
-        // 为HTTP请求创建更简洁的日志消息
-        let statusIndicator = '';
-        if (config.colorize) {
-            if (status >= 500) statusIndicator = COLORS.red;
-            else if (status >= 400) statusIndicator = COLORS.yellow;
-            else if (status >= 300) statusIndicator = COLORS.cyan;
-            else if (status >= 200) statusIndicator = COLORS.green;
-            statusIndicator += status + COLORS.reset;
-        } else {
-            statusIndicator = status;
-        }
-        
-        // 简化的请求日志格式
-        const message = `${method} ${url} ${statusIndicator} ${duration}ms`;
-        
-        // 传递ip和userAgent作为元数据,但以简洁方式显示
-        log(level, message, { ip, userAgent });
+    // 如果details为空对象,则设为null
+    if (Object.keys(details).length === 0) {
+      details = null;
     }
-};
+  }
+  
+  log(level, logMessage, details);
+}
 
-// 初始化
-ensureLogDirectory().catch(err => {
-    console.error(`${COLORS.red}Failed to initialize logger: ${err.message}${COLORS.reset}`);
-});
+// 设置日志级别
+function setLogLevel(level) {
+  if (LOG_LEVELS[level]) {
+    LOG_CONFIG.level = level;
+    log('INFO', `日志级别已设置为 ${level}`);
+    return true;
+  }
+  log('WARN', `尝试设置无效的日志级别: ${level}`);
+  return false;
+}
 
-module.exports = logger;
+// 公开各类日志记录函数
+module.exports = {
+  trace: (message, details) => log('TRACE', message, details),
+  debug: (message, details) => log('DEBUG', message, details),
+  info: (message, details) => log('INFO', message, details),
+  success: (message, details) => log('SUCCESS', message, details),
+  warn: (message, details) => log('WARN', message, details),
+  error: (message, details) => log('ERROR', message, details),
+  fatal: (message, details) => log('FATAL', message, details),
+  request,
+  setLogLevel,
+  LOG_LEVELS: Object.keys(LOG_LEVELS),
+  config: LOG_CONFIG
+};

+ 90 - 0
hubcmdui/middleware/auth.js

@@ -0,0 +1,90 @@
+/**
+ * 认证相关中间件
+ */
+const logger = require('../logger');
+
+/**
+ * 检查是否已登录的中间件
+ */
+function requireLogin(req, res, next) {
+    // 放开session检查,不强制要求登录
+    if (req.url.startsWith('/api/documentation') || 
+        req.url.startsWith('/api/system-resources') || 
+        req.url.startsWith('/api/monitoring-config') || 
+        req.url.startsWith('/api/toggle-monitoring') || 
+        req.url.startsWith('/api/test-notification') ||
+        req.url.includes('/docker/status')) {
+        return next(); // 这些API路径不需要登录
+    }
+    
+    // 检查用户是否登录
+    if (req.session && req.session.user) {
+        // 刷新会话
+        req.session.touch();
+        return next();
+    }
+    
+    // 未登录返回401错误
+    res.status(401).json({ error: '未登录或会话已过期', code: 'SESSION_EXPIRED' });
+}
+
+// 修改登录逻辑
+async function login(req, res) {
+  try {
+    const { username, password } = req.body;
+    
+    // 简单验证
+    if (username === 'admin' && password === 'admin123') {
+      req.session.user = { username };
+      return res.json({ success: true });
+    }
+    
+    res.status(401).json({ error: '用户名或密码错误' });
+  } catch (error) {
+    logger.error('登录失败:', error);
+    res.status(500).json({ error: '登录失败' });
+  }
+}
+
+/**
+ * 记录会话活动的中间件
+ */
+function sessionActivity(req, res, next) {
+    if (req.session && req.session.user) {
+        req.session.lastActivity = Date.now();
+        req.session.touch(); // 确保会话刷新
+    }
+    next();
+}
+
+// 过滤敏感信息中间件
+function sanitizeRequestBody(req, res, next) {
+  if (req.body) {
+    const sanitizedBody = {...req.body};
+    
+    // 过滤敏感字段
+    if (sanitizedBody.password) sanitizedBody.password = '[REDACTED]';
+    if (sanitizedBody.currentPassword) sanitizedBody.currentPassword = '[REDACTED]';
+    if (sanitizedBody.newPassword) sanitizedBody.newPassword = '[REDACTED]';
+    
+    // 保存清理后的请求体供日志使用
+    req.sanitizedBody = sanitizedBody;
+  }
+  next();
+}
+
+// 安全头部中间件
+function securityHeaders(req, res, next) {
+  // 添加安全头部
+  res.setHeader('X-Content-Type-Options', 'nosniff');
+  res.setHeader('X-Frame-Options', 'DENY');
+  res.setHeader('X-XSS-Protection', '1; mode=block');
+  next();
+}
+
+module.exports = {
+  requireLogin,
+  sessionActivity,
+  sanitizeRequestBody,
+  securityHeaders
+};

+ 26 - 0
hubcmdui/middleware/client-error.js

@@ -0,0 +1,26 @@
+/**
+ * 客户端错误处理中间件
+ */
+const logger = require('../logger');
+
+// 处理客户端上报的错误
+function handleClientError(req, res, next) {
+  if (req.url === '/api/client-error' && req.method === 'POST') {
+    const { message, source, lineno, colno, error, stack, userAgent, page } = req.body;
+    
+    logger.error('客户端错误:', {
+      message,
+      source,
+      location: `${lineno}:${colno}`,
+      stack: stack || (error && error.stack),
+      userAgent,
+      page
+    });
+    
+    res.json({ success: true });
+  } else {
+    next();
+  }
+}
+
+module.exports = handleClientError;

+ 13 - 0
hubcmdui/models/MenuItem.js

@@ -0,0 +1,13 @@
+const mongoose = require('mongoose');
+
+const menuItemSchema = new mongoose.Schema({
+    text: { type: String, required: true },
+    link: { type: String, required: true },
+    icon: String,
+    newTab: { type: Boolean, default: false },
+    enabled: { type: Boolean, default: true },
+    order: { type: Number, default: 0 },
+    createdAt: { type: Date, default: Date.now }
+});
+
+module.exports = mongoose.model('MenuItem', menuItemSchema);

+ 37 - 11
hubcmdui/package.json

@@ -1,17 +1,43 @@
 {
+  "name": "hubcmdui",
+  "version": "1.0.0",
+  "description": "Docker镜像代理加速系统",
+  "main": "server.js",
+  "scripts": {
+    "start": "node server.js",
+    "dev": "nodemon server.js",
+    "test": "jest",
+    "init": "node scripts/init-system.js",
+    "setup": "npm install && node scripts/init-system.js && echo '系统安装完成,请使用 npm start 启动服务'"
+  },
+  "keywords": [
+    "docker",
+    "proxy",
+    "management"
+  ],
+  "author": "",
+  "license": "MIT",
   "dependencies": {
-    "axios": "^1.7.5",
-    "axios-retry": "^3.5.0",
-    "bcrypt": "^5.1.1",
-    "chalk": "^5.3.0",
+    "axios": "^0.27.2",
+    "axios-retry": "^3.3.1",
+    "bcrypt": "^5.0.1",
+    "body-parser": "^1.20.0",
+    "chalk": "^4.1.2",
     "cors": "^2.8.5",
-    "dockerode": "^4.0.2",
-    "express": "^4.19.2",
-    "express-session": "^1.18.0",
-    "morgan": "^1.10.0",
+    "dockerode": "^3.3.4",
+    "express": "^4.21.2",
+    "express-session": "^1.18.1",
     "node-cache": "^5.1.2",
-    "p-limit": "^3.1.0",
-    "validator": "^13.12.0",
-    "ws": "^8.18.0"
+    "p-limit": "^4.0.0",
+    "session-file-store": "^1.5.0",
+    "validator": "^13.7.0",
+    "ws": "^8.8.1"
+  },
+  "devDependencies": {
+    "jest": "^28.1.3",
+    "nodemon": "^2.0.19"
+  },
+  "engines": {
+    "node": ">=14.0.0"
   }
 }

+ 155 - 0
hubcmdui/routes/auth.js

@@ -0,0 +1,155 @@
+/**
+ * 认证相关路由
+ */
+const express = require('express');
+const router = express.Router();
+const bcrypt = require('bcrypt');
+const userService = require('../services/userService');
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+
+// 登录验证
+router.post('/login', async (req, res) => {
+  const { username, password, captcha } = req.body;
+  if (req.session.captcha !== parseInt(captcha)) {
+    logger.warn(`Captcha verification failed for user: ${username}`);
+    return res.status(401).json({ error: '验证码错误' });
+  }
+
+  try {
+    const users = await userService.getUsers();
+    const user = users.users.find(u => u.username === username);
+    
+    if (!user) {
+      logger.warn(`User ${username} not found`);
+      return res.status(401).json({ error: '用户名或密码错误' });
+    }
+
+    if (bcrypt.compareSync(req.body.password, user.password)) {
+      req.session.user = { username: user.username };
+      
+      // 更新用户登录信息
+      await userService.updateUserLoginInfo(username);
+      
+      // 确保服务器启动时间已设置
+      if (!global.serverStartTime) {
+        global.serverStartTime = Date.now();
+        logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`);
+      }
+      
+      logger.info(`User ${username} logged in successfully`);
+      res.json({ 
+        success: true,
+        serverStartTime: global.serverStartTime
+      });
+    } else {
+      logger.warn(`Login failed for user: ${username}`);
+      res.status(401).json({ error: '用户名或密码错误' });
+    }
+  } catch (error) {
+    logger.error('登录失败:', error);
+    res.status(500).json({ error: '登录处理失败', details: error.message });
+  }
+});
+
+// 注销
+router.post('/logout', (req, res) => {
+  req.session.destroy(err => {
+    if (err) {
+      logger.error('销毁会话失败:', err);
+      return res.status(500).json({ error: 'Failed to logout' });
+    }
+    res.clearCookie('connect.sid');
+    logger.info('用户已退出登录');
+    res.json({ success: true });
+  });
+});
+
+// 修改密码
+router.post('/change-password', requireLogin, async (req, res) => {
+  const { currentPassword, newPassword } = req.body;
+  
+  // 密码复杂度校验
+  const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
+  if (!passwordRegex.test(newPassword)) {
+    return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter, one number, and one special character' });
+  }
+  
+  try {
+    const { users } = await userService.getUsers();
+    const user = users.find(u => u.username === req.session.user.username);
+    
+    if (user && bcrypt.compareSync(currentPassword, user.password)) {
+      user.password = bcrypt.hashSync(newPassword, 10);
+      await userService.saveUsers(users);
+      res.json({ success: true });
+    } else {
+      res.status(401).json({ error: 'Invalid current password' });
+    }
+  } catch (error) {
+    logger.error('修改密码失败:', error);
+    res.status(500).json({ error: '修改密码失败', details: error.message });
+  }
+});
+
+// 获取用户信息
+router.get('/user-info', requireLogin, async (req, res) => {
+  try {
+    const userService = require('../services/userService');
+    const userStats = await userService.getUserStats(req.session.user.username);
+    
+    res.json(userStats);
+  } catch (error) {
+    logger.error('获取用户信息失败:', error);
+    res.status(500).json({ error: '获取用户信息失败', details: error.message });
+  }
+});
+
+// 生成验证码
+router.get('/captcha', (req, res) => {
+  const num1 = Math.floor(Math.random() * 10);
+  const num2 = Math.floor(Math.random() * 10);
+  const captcha = `${num1} + ${num2} = ?`;
+  req.session.captcha = num1 + num2;
+  
+  // 确保serverStartTime已初始化
+  if (!global.serverStartTime) {
+    global.serverStartTime = Date.now();
+    logger.warn(`初始化服务器启动时间: ${global.serverStartTime}`);
+  }
+  
+  res.json({ 
+    captcha,
+    serverStartTime: global.serverStartTime
+  });
+});
+
+// 检查会话状态
+router.get('/check-session', (req, res) => {
+  // 如果global.serverStartTime不存在,创建一个
+  if (!global.serverStartTime) {
+    global.serverStartTime = Date.now();
+    logger.warn(`设置服务器启动时间: ${global.serverStartTime}`);
+  }
+
+  if (req.session && req.session.user) {
+    return res.json({
+      success: true,
+      user: {
+        username: req.session.user.username,
+        role: req.session.user.role,
+      },
+      serverStartTime: global.serverStartTime // 返回服务器启动时间
+    });
+  }
+  return res.status(401).json({ 
+    success: false, 
+    message: '未登录',
+    serverStartTime: global.serverStartTime // 即使未登录也返回服务器时间
+  });
+});
+
+logger.success('✓ 认证路由已加载');
+
+// 导出路由
+module.exports = router;

+ 224 - 0
hubcmdui/routes/config.js

@@ -0,0 +1,224 @@
+/**
+ * 配置路由模块
+ */
+const express = require('express');
+const router = express.Router();
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+const configService = require('../services/configService');
+
+// 修改配置文件路径,使用独立的配置文件
+const configFilePath = path.join(__dirname, '../data/config.json');
+
+// 默认配置
+const DEFAULT_CONFIG = {
+    proxyDomain: 'registry-1.docker.io',
+    logo: '',
+    theme: 'light'
+};
+
+// 确保配置文件存在
+async function ensureConfigFile() {
+    try {
+        // 确保目录存在
+        const dir = path.dirname(configFilePath);
+        try {
+            await fs.access(dir);
+        } catch (error) {
+            await fs.mkdir(dir, { recursive: true });
+            logger.info(`创建目录: ${dir}`);
+        }
+        
+        // 检查文件是否存在
+        try {
+            await fs.access(configFilePath);
+            const data = await fs.readFile(configFilePath, 'utf8');
+            return JSON.parse(data);
+        } catch (error) {
+            // 文件不存在或JSON解析错误,创建默认配置
+            await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
+            logger.info(`创建默认配置文件: ${configFilePath}`);
+            return DEFAULT_CONFIG;
+        }
+    } catch (error) {
+        logger.error(`配置文件操作失败: ${error.message}`);
+        // 出错时返回默认配置以确保API不会失败
+        return DEFAULT_CONFIG;
+    }
+}
+
+// 获取配置
+router.get('/config', async (req, res) => {
+    try {
+        const config = await ensureConfigFile();
+        res.json(config);
+    } catch (error) {
+        logger.error('获取配置失败:', error);
+        // 即使失败也返回默认配置
+        res.json(DEFAULT_CONFIG);
+    }
+});
+
+// 保存配置
+router.post('/config', async (req, res) => {
+    try {
+        const newConfig = req.body;
+        
+        // 验证请求数据
+        if (!newConfig || typeof newConfig !== 'object') {
+            return res.status(400).json({
+                error: '无效的配置数据',
+                details: '配置必须是一个对象'
+            });
+        }
+        
+        // 读取现有配置
+        let existingConfig;
+        try {
+            existingConfig = await ensureConfigFile();
+        } catch (error) {
+            existingConfig = DEFAULT_CONFIG;
+        }
+        
+        // 合并配置
+        const mergedConfig = { ...existingConfig, ...newConfig };
+        
+        // 保存到文件
+        await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2));
+        
+        res.json({ success: true, message: '配置已保存' });
+    } catch (error) {
+        logger.error('保存配置失败:', error);
+        res.status(500).json({ 
+            error: '保存配置失败',
+            details: error.message 
+        });
+    }
+});
+
+// 获取监控配置
+router.get('/monitoring-config', async (req, res) => {
+  logger.info('收到监控配置请求');
+  
+  try {
+    logger.info('读取监控配置...');
+    const config = await configService.getConfig();
+    
+    if (!config.monitoringConfig) {
+      logger.info('监控配置不存在,创建默认配置');
+      config.monitoringConfig = {
+        notificationType: 'wechat',
+        webhookUrl: '',
+        telegramToken: '',
+        telegramChatId: '',
+        monitorInterval: 60,
+        isEnabled: false
+      };
+      await configService.saveConfig(config);
+    }
+    
+    logger.info('返回监控配置');
+    res.json({
+      notificationType: config.monitoringConfig.notificationType || 'wechat',
+      webhookUrl: config.monitoringConfig.webhookUrl || '',
+      telegramToken: config.monitoringConfig.telegramToken || '',
+      telegramChatId: config.monitoringConfig.telegramChatId || '',
+      monitorInterval: config.monitoringConfig.monitorInterval || 60,
+      isEnabled: config.monitoringConfig.isEnabled || false
+    });
+  } catch (error) {
+    logger.error('获取监控配置失败:', error);
+    res.status(500).json({ error: '获取监控配置失败', details: error.message });
+  }
+});
+
+// 保存监控配置
+router.post('/monitoring-config', requireLogin, async (req, res) => {
+  try {
+    const { 
+      notificationType, 
+      webhookUrl, 
+      telegramToken, 
+      telegramChatId, 
+      monitorInterval, 
+      isEnabled 
+    } = req.body;
+    
+    // 验证必填字段
+    if (!notificationType) {
+      return res.status(400).json({ error: '通知类型不能为空' });
+    }
+    
+    // 根据通知类型验证对应的字段
+    if (notificationType === 'wechat' && !webhookUrl) {
+      return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' });
+    }
+    
+    if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+      return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' });
+    }
+    
+    // 保存配置
+    const config = await configService.getConfig();
+    config.monitoringConfig = {
+      notificationType,
+      webhookUrl: webhookUrl || '',
+      telegramToken: telegramToken || '',
+      telegramChatId: telegramChatId || '',
+      monitorInterval: parseInt(monitorInterval) || 60,
+      isEnabled: !!isEnabled
+    };
+    
+    await configService.saveConfig(config);
+    logger.info('监控配置已更新');
+    
+    res.json({ success: true, message: '监控配置已保存' });
+  } catch (error) {
+    logger.error('保存监控配置失败:', error);
+    res.status(500).json({ error: '保存监控配置失败', details: error.message });
+  }
+});
+
+// 测试通知
+router.post('/test-notification', requireLogin, async (req, res) => {
+  try {
+    const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body;
+    
+    // 验证参数
+    if (!notificationType) {
+      return res.status(400).json({ error: '通知类型不能为空' });
+    }
+    
+    if (notificationType === 'wechat' && !webhookUrl) {
+      return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' });
+    }
+    
+    if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+      return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' });
+    }
+    
+    // 构造测试消息
+    const testMessage = {
+      title: '测试通知',
+      content: `这是一条测试通知消息,发送时间: ${new Date().toLocaleString('zh-CN')}`,
+      type: 'info'
+    };
+    
+    // 模拟发送通知
+    logger.info('发送测试通知:', testMessage);
+    
+    // TODO: 实际发送通知的逻辑
+    // 这里仅做模拟,实际应用中需要实现真正的通知发送逻辑
+    
+    // 返回成功
+    res.json({ success: true, message: '测试通知已发送' });
+  } catch (error) {
+    logger.error('发送测试通知失败:', error);
+    res.status(500).json({ error: '发送测试通知失败', details: error.message });
+  }
+});
+
+// 导出路由
+module.exports = router;

+ 146 - 0
hubcmdui/routes/docker.js

@@ -0,0 +1,146 @@
+/**
+ * Docker容器管理路由
+ */
+const express = require('express');
+const router = express.Router();
+const WebSocket = require('ws');
+const http = require('http');
+const dockerService = require('../services/dockerService');
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+
+// 获取Docker状态
+router.get('/status', requireLogin, async (req, res) => {
+  try {
+    const containerStatus = await dockerService.getContainersStatus();
+    res.json(containerStatus);
+  } catch (error) {
+    logger.error('获取 Docker 状态时出错:', error);
+    res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
+  }
+});
+
+// 获取单个容器状态
+router.get('/status/:id', requireLogin, async (req, res) => {
+  try {
+    const containerInfo = await dockerService.getContainerStatus(req.params.id);
+    res.json(containerInfo);
+  } catch (error) {
+    logger.error('获取容器状态失败:', error);
+    res.status(500).json({ error: '获取容器状态失败', details: error.message });
+  }
+});
+
+// 重启容器
+router.post('/restart/:id', requireLogin, async (req, res) => {
+  try {
+    await dockerService.restartContainer(req.params.id);
+    res.json({ success: true });
+  } catch (error) {
+    logger.error('重启容器失败:', error);
+    res.status(500).json({ error: '重启容器失败', details: error.message });
+  }
+});
+
+// 停止容器
+router.post('/stop/:id', requireLogin, async (req, res) => {
+  try {
+    await dockerService.stopContainer(req.params.id);
+    res.json({ success: true });
+  } catch (error) {
+    logger.error('停止容器失败:', error);
+    res.status(500).json({ error: '停止容器失败', details: error.message });
+  }
+});
+
+// 删除容器
+router.post('/delete/:id', requireLogin, async (req, res) => {
+  try {
+    await dockerService.deleteContainer(req.params.id);
+    res.json({ success: true, message: '容器已成功删除' });
+  } catch (error) {
+    logger.error('删除容器失败:', error);
+    res.status(500).json({ error: '删除容器失败', details: error.message });
+  }
+});
+
+// 更新容器
+router.post('/update/:id', requireLogin, async (req, res) => {
+  try {
+    const { tag } = req.body;
+    await dockerService.updateContainer(req.params.id, tag);
+    res.json({ success: true, message: '容器更新成功' });
+  } catch (error) {
+    logger.error('更新容器失败:', error);
+    res.status(500).json({ error: '更新容器失败', details: error.message, stack: error.stack });
+  }
+});
+
+// 获取已停止容器
+router.get('/stopped', requireLogin, async (req, res) => {
+  try {
+    const stoppedContainers = await dockerService.getStoppedContainers();
+    res.json(stoppedContainers);
+  } catch (error) {
+    logger.error('获取已停止容器列表失败:', error);
+    res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
+  }
+});
+
+// 获取容器日志(HTTP轮询)
+router.get('/logs-poll/:id', async (req, res) => {
+  const { id } = req.params;
+  try {
+    const logs = await dockerService.getContainerLogs(id);
+    res.send(logs);
+  } catch (error) {
+    logger.error('获取容器日志失败:', error);
+    res.status(500).send('获取日志失败');
+  }
+});
+
+// 设置WebSocket路由,用于实时日志流
+function setupLogWebsocket(server) {
+  const wss = new WebSocket.Server({ server });
+  
+  wss.on('connection', async (ws, req) => {
+    try {
+      const containerId = req.url.split('/').pop();
+      const docker = await dockerService.getDockerConnection();
+      
+      if (!docker) {
+        ws.send('Error: 无法连接到 Docker 守护进程');
+        return;
+      }
+      
+      const container = docker.getContainer(containerId);
+      const stream = await container.logs({
+        follow: true,
+        stdout: true,
+        stderr: true,
+        tail: 100
+      });
+      
+      stream.on('data', (chunk) => {
+        const cleanedChunk = chunk.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
+        // 移除不可打印字符
+        const printableChunk = cleanedChunk.replace(/[^\x20-\x7E\x0A\x0D]/g, '');
+        ws.send(printableChunk);
+      });
+      
+      ws.on('close', () => {
+        stream.destroy();
+      });
+      
+      stream.on('error', (err) => {
+        ws.send('Error: ' + err.message);
+      });
+    } catch (err) {
+      ws.send('Error: ' + err.message);
+    }
+  });
+}
+
+// 直接导出 router 实例,并添加 setupLogWebsocket 作为静态属性
+router.setupLogWebsocket = setupLogWebsocket;
+module.exports = router;

+ 65 - 0
hubcmdui/routes/dockerhub.js

@@ -0,0 +1,65 @@
+/**
+ * Docker Hub 代理路由
+ */
+const express = require('express');
+const router = express.Router();
+const axios = require('axios');
+const logger = require('../logger');
+
+// Docker Hub API 基础 URL
+const DOCKER_HUB_API = 'https://hub.docker.com/v2';
+
+// 搜索镜像
+router.get('/search', async (req, res) => {
+    try {
+        const { term, page = 1, limit = 25 } = req.query;
+        
+        // 确保有搜索关键字
+        if (!term) {
+            return res.status(400).json({ error: '缺少搜索关键字(term)' });
+        }
+        
+        logger.info(`搜索 Docker Hub: 关键字="${term}", 页码=${page}`);
+        
+        const response = await axios.get(`${DOCKER_HUB_API}/search/repositories`, {
+            params: {
+                query: term,
+                page,
+                page_size: limit
+            },
+            timeout: 10000
+        });
+        
+        res.json(response.data);
+    } catch (err) {
+        logger.error('Docker Hub 搜索失败:', err.message);
+        res.status(500).json({ error: 'Docker Hub 搜索失败', details: err.message });
+    }
+});
+
+// 获取镜像标签
+router.get('/tags/:owner/:repo', async (req, res) => {
+    try {
+        const { owner, repo } = req.params;
+        const { page = 1, limit = 25 } = req.query;
+        
+        logger.info(`获取镜像标签: ${owner}/${repo}, 页码=${page}`);
+        
+        const response = await axios.get(
+            `${DOCKER_HUB_API}/repositories/${owner}/${repo}/tags`, {
+            params: {
+                page,
+                page_size: limit
+            },
+            timeout: 10000
+        });
+        
+        res.json(response.data);
+    } catch (err) {
+        logger.error('获取 Docker 镜像标签失败:', err.message);
+        res.status(500).json({ error: '获取镜像标签失败', details: err.message });
+    }
+});
+
+// 直接导出路由实例
+module.exports = router;

+ 537 - 0
hubcmdui/routes/documentation.js

@@ -0,0 +1,537 @@
+/**
+ * 文档管理路由
+ */
+const express = require('express');
+const router = express.Router();
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+
+// 确保文档目录存在
+const docsDir = path.join(__dirname, '../documentation');
+const metaDir = path.join(docsDir, 'meta');
+
+// 文档文件扩展名
+const FILE_EXTENSION = '.md';
+const META_EXTENSION = '.json';
+
+// 确保目录存在
+async function ensureDirectories() {
+    try {
+        await fs.mkdir(docsDir, { recursive: true });
+        await fs.mkdir(metaDir, { recursive: true });
+    } catch (err) {
+        logger.error('创建文档目录失败:', err);
+    }
+}
+
+// 读取文档元数据
+async function getDocumentMeta(id) {
+    const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
+    try {
+        const metaContent = await fs.readFile(metaPath, 'utf8');
+        return JSON.parse(metaContent);
+    } catch (err) {
+        // 如果元数据文件不存在,返回默认值
+        return {
+            id,
+            published: false,
+            createdAt: new Date().toISOString(),
+            updatedAt: new Date().toISOString()
+        };
+    }
+}
+
+// 保存文档元数据
+async function saveDocumentMeta(id, metadata) {
+    await ensureDirectories();
+    const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
+    const metaContent = JSON.stringify(metadata, null, 2);
+    await fs.writeFile(metaPath, metaContent);
+}
+
+// 初始化确保目录存在,但不再创建默认文档
+(async function() {
+    try {
+        await ensureDirectories();
+        logger.info('文档目录已初始化');
+    } catch (error) {
+        logger.error('初始化文档目录失败:', error);
+    }
+})();
+
+// 获取所有文档列表
+router.get('/documents', async (req, res) => {
+    try {
+        let files;
+        try {
+            files = await fs.readdir(docsDir);
+        } catch (err) {
+            // 如果目录不存在,尝试创建它并返回空列表
+            if (err.code === 'ENOENT') {
+                await fs.mkdir(docsDir, { recursive: true });
+                files = [];
+            } else {
+                throw err;
+            }
+        }
+        
+        const documents = [];
+        for (const file of files) {
+            if (file.endsWith(FILE_EXTENSION)) {
+                const filePath = path.join(docsDir, file);
+                const stats = await fs.stat(filePath);
+                const content = await fs.readFile(filePath, 'utf8');
+                const id = file.replace(FILE_EXTENSION, '');
+                
+                // 读取元数据
+                const metadata = await getDocumentMeta(id);
+                
+                // 解析文档元数据(简单实现)
+                const titleMatch = content.match(/^#\s+(.*)$/m);
+                const title = titleMatch ? titleMatch[1] : id;
+                
+                documents.push({
+                    id,
+                    title,
+                    content,
+                    createdAt: metadata.createdAt || stats.birthtime,
+                    updatedAt: metadata.updatedAt || stats.mtime,
+                    published: metadata.published || false
+                });
+            }
+        }
+        
+        res.json(documents);
+    } catch (err) {
+        logger.error('获取文档列表失败:', err);
+        res.status(500).json({ error: '获取文档列表失败' });
+    }
+});
+
+// 保存文档
+router.put('/documents/:id', requireLogin, async (req, res) => {
+    try {
+        const { id } = req.params;
+        const { title, content, published } = req.body;
+        
+        if (!title || !content) {
+            return res.status(400).json({ error: '标题和内容为必填项' });
+        }
+        
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        
+        // 确保文档目录存在
+        await ensureDirectories();
+        
+        // 获取或创建元数据
+        const metadata = await getDocumentMeta(id);
+        metadata.title = title;
+        metadata.published = typeof published === 'boolean' ? published : metadata.published;
+        metadata.updatedAt = new Date().toISOString();
+        
+        // 保存文档内容
+        await fs.writeFile(filePath, content);
+        
+        // 保存元数据
+        await saveDocumentMeta(id, metadata);
+        
+        // 获取文件状态
+        const stats = await fs.stat(filePath);
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: metadata.createdAt,
+            updatedAt: new Date().toISOString(),
+            published: metadata.published
+        };
+        
+        res.json(document);
+    } catch (err) {
+        logger.error('保存文档失败:', err);
+        res.status(500).json({ error: '保存文档失败', details: err.message });
+    }
+});
+
+// 创建新文档
+router.post('/documents', requireLogin, async (req, res) => {
+    try {
+        const { title, content, published } = req.body;
+        
+        if (!title || !content) {
+            return res.status(400).json({ error: '标题和内容为必填项' });
+        }
+        
+        // 生成唯一ID
+        const id = Date.now().toString();
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        
+        // 确保文档目录存在
+        await ensureDirectories();
+        
+        // 创建元数据
+        const now = new Date().toISOString();
+        const metadata = {
+            id,
+            title,
+            published: typeof published === 'boolean' ? published : false,
+            createdAt: now,
+            updatedAt: now
+        };
+        
+        // 保存文档内容
+        await fs.writeFile(filePath, content);
+        
+        // 保存元数据
+        await saveDocumentMeta(id, metadata);
+        
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: now,
+            updatedAt: now,
+            published: metadata.published
+        };
+        
+        res.status(201).json(document);
+    } catch (err) {
+        logger.error('创建文档失败:', err);
+        res.status(500).json({ error: '创建文档失败', details: err.message });
+    }
+});
+
+// 删除文档
+router.delete('/documents/:id', requireLogin, async (req, res) => {
+    try {
+        const { id } = req.params;
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
+        
+        let success = false;
+        
+        // 尝试删除主文档文件
+        try {
+            await fs.access(filePath);
+            await fs.unlink(filePath);
+            success = true;
+            logger.info(`文档 ${id} 已成功删除`);
+        } catch (err) {
+            logger.warn(`删除文档文件 ${id} 失败:`, err);
+        }
+        
+        // 尝试删除元数据文件
+        try {
+            await fs.access(metaPath);
+            await fs.unlink(metaPath);
+            success = true;
+            logger.info(`文档元数据 ${id} 已成功删除`);
+        } catch (err) {
+            logger.warn(`删除文档元数据 ${id} 失败:`, err);
+        }
+        
+        if (success) {
+            res.json({ success: true });
+        } else {
+            throw new Error('文档和元数据均不存在或无法删除');
+        }
+    } catch (err) {
+        logger.error(`删除文档 ${req.params.id} 失败:`, err);
+        res.status(500).json({ error: '删除文档失败', details: err.message });
+    }
+});
+
+// 获取单个文档
+router.get('/documents/:id', async (req, res) => {
+    try {
+        const { id } = req.params;
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        
+        // 检查文件是否存在
+        try {
+            await fs.access(filePath);
+        } catch (err) {
+            return res.status(404).json({ error: '文档不存在' });
+        }
+        
+        // 读取文件内容和元数据
+        const content = await fs.readFile(filePath, 'utf8');
+        const metadata = await getDocumentMeta(id);
+        
+        // 解析文档标题
+        const titleMatch = content.match(/^#\s+(.*)$/m);
+        const title = titleMatch ? titleMatch[1] : metadata.title || id;
+        
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: metadata.createdAt,
+            updatedAt: metadata.updatedAt,
+            published: metadata.published
+        };
+        
+        res.json(document);
+    } catch (err) {
+        logger.error(`获取文档 ${req.params.id} 失败:`, err);
+        res.status(500).json({ error: '获取文档失败', details: err.message });
+    }
+});
+
+// 更新文档发布状态
+router.put('/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
+    try {
+        const { id } = req.params;
+        const { published } = req.body;
+        
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        
+        // 检查文件是否存在
+        try {
+            await fs.access(filePath);
+        } catch (err) {
+            return res.status(404).json({ error: '文档不存在' });
+        }
+        
+        // 读取文件内容和元数据
+        const content = await fs.readFile(filePath, 'utf8');
+        const metadata = await getDocumentMeta(id);
+        
+        // 更新元数据
+        metadata.published = typeof published === 'boolean' ? published : !metadata.published;
+        metadata.updatedAt = new Date().toISOString();
+        
+        // 保存元数据
+        await saveDocumentMeta(id, metadata);
+        
+        // 解析文档标题
+        const titleMatch = content.match(/^#\s+(.*)$/m);
+        const title = titleMatch ? titleMatch[1] : metadata.title || id;
+        
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: metadata.createdAt,
+            updatedAt: metadata.updatedAt,
+            published: metadata.published
+        };
+        
+        res.json(document);
+    } catch (err) {
+        logger.error(`更新文档状态 ${req.params.id} 失败:`, err);
+        res.status(500).json({ error: '更新文档状态失败', details: err.message });
+    }
+});
+
+// 为前端添加获取已发布文档列表的路由
+router.get('/documentation', async (req, res) => {
+    try {
+        let files;
+        try {
+            files = await fs.readdir(docsDir);
+        } catch (err) {
+            if (err.code === 'ENOENT') {
+                await fs.mkdir(docsDir, { recursive: true });
+                files = [];
+            } else {
+                throw err;
+            }
+        }
+        
+        const documents = [];
+        for (const file of files) {
+            if (file.endsWith(FILE_EXTENSION)) {
+                const filePath = path.join(docsDir, file);
+                const stats = await fs.stat(filePath);
+                const id = file.replace(FILE_EXTENSION, '');
+                
+                // 读取元数据
+                const metadata = await getDocumentMeta(id);
+                
+                // 如果发布状态为true,则包含在返回结果中
+                if (metadata.published === true) {
+                    const content = await fs.readFile(filePath, 'utf8');
+                    
+                    // 解析文档标题
+                    const titleMatch = content.match(/^#\s+(.*)$/m);
+                    const title = titleMatch ? titleMatch[1] : metadata.title || id;
+                    
+                    documents.push({
+                        id,
+                        title,
+                        createdAt: metadata.createdAt || stats.birthtime,
+                        updatedAt: metadata.updatedAt || stats.mtime,
+                        published: true
+                    });
+                }
+            }
+        }
+        
+        logger.info(`前端请求文档列表,返回 ${documents.length} 个已发布文档`);
+        res.json(documents);
+    } catch (err) {
+        logger.error('获取前端文档列表失败:', err);
+        res.status(500).json({ error: '获取文档列表失败' });
+    }
+});
+
+// 前端获取单个文档内容
+router.get('/documentation/:id', async (req, res) => {
+    try {
+        const { id } = req.params;
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        
+        // 检查文件是否存在
+        try {
+            await fs.access(filePath);
+        } catch (err) {
+            return res.status(404).json({ error: '文档不存在' });
+        }
+        
+        // 读取文件内容和元数据
+        const content = await fs.readFile(filePath, 'utf8');
+        const metadata = await getDocumentMeta(id);
+        
+        // 如果文档未发布,则不允许前端访问
+        if (metadata.published !== true) {
+            return res.status(403).json({ error: '该文档未发布,无法访问' });
+        }
+        
+        // 解析文档标题
+        const titleMatch = content.match(/^#\s+(.*)$/m);
+        const title = titleMatch ? titleMatch[1] : metadata.title || id;
+        
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: metadata.createdAt,
+            updatedAt: metadata.updatedAt,
+            published: true
+        };
+        
+        logger.info(`前端请求文档: ${id} - ${title}`);
+        res.json(document);
+    } catch (err) {
+        logger.error(`获取前端文档内容 ${req.params.id} 失败:`, err);
+        res.status(500).json({ error: '获取文档内容失败', details: err.message });
+    }
+});
+
+// 修改发布状态
+router.patch('/documents/:id/publish', requireLogin, async (req, res) => {
+    try {
+        const { id } = req.params;
+        const { published } = req.body;
+        
+        if (typeof published !== 'boolean') {
+            return res.status(400).json({ error: '发布状态必须为布尔值' });
+        }
+        
+        // 获取或创建元数据
+        const metadata = await getDocumentMeta(id);
+        metadata.published = published;
+        metadata.updatedAt = new Date().toISOString();
+        
+        // 保存元数据
+        await saveDocumentMeta(id, metadata);
+        
+        // 获取文档内容
+        const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
+        let content;
+        try {
+            content = await fs.readFile(filePath, 'utf8');
+        } catch (err) {
+            if (err.code === 'ENOENT') {
+                return res.status(404).json({ error: '文档不存在' });
+            }
+            throw err;
+        }
+        
+        // 解析标题
+        const titleMatch = content.match(/^#\s+(.*)$/m);
+        const title = titleMatch ? titleMatch[1] : id;
+        
+        // 返回更新后的文档信息
+        const document = {
+            id,
+            title,
+            content,
+            createdAt: metadata.createdAt,
+            updatedAt: metadata.updatedAt,
+            published: metadata.published
+        };
+        
+        res.json(document);
+    } catch (err) {
+        logger.error('修改发布状态失败:', err);
+        res.status(500).json({ error: '修改发布状态失败', details: err.message });
+    }
+});
+
+// 获取单个文档文件内容
+router.get('/file', async (req, res) => {
+    try {
+        const filePath = req.query.path;
+        
+        if (!filePath) {
+            return res.status(400).json({ error: '文件路径不能为空' });
+        }
+        
+        logger.info(`请求获取文档文件: ${filePath}`);
+        
+        // 尝试直接从文件系统读取文件
+        try {
+            // 安全检查,确保只能访问documentation目录下的文件
+            const fileName = path.basename(filePath);
+            const fileDir = path.dirname(filePath);
+            
+            // 构建完整路径(只允许访问documentation目录)
+            const fullPath = path.join(__dirname, '..', 'documentation', fileName);
+            
+            logger.info(`尝试读取文件: ${fullPath}`);
+            
+            // 检查文件是否存在
+            const fileExists = await fs.access(fullPath)
+                .then(() => true)
+                .catch(() => false);
+            
+            // 如果文件不存在,则返回错误
+            if (!fileExists) {
+                logger.warn(`文件不存在: ${fullPath}`);
+                return res.status(404).json({ error: '文档不存在' });
+            }
+            
+            logger.info(`文件存在,开始读取: ${fullPath}`);
+            
+            // 读取文件内容
+            const fileContent = await fs.readFile(fullPath, 'utf8');
+            
+            // 设置适当的Content-Type
+            if (fileName.endsWith('.md')) {
+                res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
+            } else if (fileName.endsWith('.json')) {
+                res.setHeader('Content-Type', 'application/json; charset=utf-8');
+            } else {
+                res.setHeader('Content-Type', 'text/plain; charset=utf-8');
+            }
+            
+            logger.info(`成功读取文件,内容长度: ${fileContent.length}`);
+            return res.send(fileContent);
+        } catch (error) {
+            logger.error(`读取文件失败: ${error.message}`, error);
+            return res.status(500).json({ error: `读取文件失败: ${error.message}` });
+        }
+        
+    } catch (error) {
+        logger.error('获取文档文件失败:', error);
+        res.status(500).json({ error: '获取文档文件失败', details: error.message });
+    }
+});
+
+// 导出路由
+logger.success('✓ 文档管理路由已加载');
+module.exports = router;

+ 48 - 0
hubcmdui/routes/health.js

@@ -0,0 +1,48 @@
+/**
+ * 健康检查路由
+ */
+const express = require('express');
+const router = express.Router();
+const os = require('os');
+const path = require('path');
+const { version } = require('../package.json');
+
+// 简单健康检查
+router.get('/', (req, res) => {
+    res.json({
+        status: 'ok',
+        uptime: process.uptime(),
+        timestamp: Date.now(),
+        version
+    });
+});
+
+// 详细系统信息
+router.get('/system', (req, res) => {
+    try {
+        res.json({
+            status: 'ok',
+            system: {
+                platform: os.platform(),
+                release: os.release(),
+                hostname: os.hostname(),
+                uptime: os.uptime(),
+                totalMem: os.totalmem(),
+                freeMem: os.freemem(),
+                cpus: os.cpus().length,
+                loadavg: os.loadavg()
+            },
+            process: {
+                pid: process.pid,
+                uptime: process.uptime(),
+                memoryUsage: process.memoryUsage(),
+                nodeVersion: process.version,
+                env: process.env.NODE_ENV || 'development'
+            }
+        });
+    } catch (err) {
+        res.status(500).json({ error: '获取系统信息失败', details: err.message });
+    }
+});
+
+module.exports = router;

+ 78 - 0
hubcmdui/routes/index.js

@@ -0,0 +1,78 @@
+/**
+ * 路由注册器
+ * 负责注册所有API路由
+ */
+const fs = require('fs');
+const path = require('path');
+const logger = require('../logger');
+
+// 检查文件是否是路由模块
+function isRouteModule(file) {
+    return file.endsWith('.js') && 
+           file !== 'index.js' && 
+           file !== 'routeLoader.js' && 
+           !file.startsWith('_');
+}
+
+/**
+ * 注册所有路由
+ * @param {Express} app - Express应用实例
+ */
+function registerRoutes(app) {
+    const routeDir = __dirname;
+    
+    try {
+        const files = fs.readdirSync(routeDir);
+        const routeFiles = files.filter(isRouteModule);
+        
+        logger.info(`发现 ${routeFiles.length} 个路由文件待加载`);
+        
+        routeFiles.forEach(file => {
+            const routeName = path.basename(file, '.js');
+            try {
+                const routePath = path.join(routeDir, file);
+                const routeExport = require(routePath); // 加载导出的模块
+                
+                // 优先处理 { router: routerInstance, ... } 格式
+                if (routeExport && typeof routeExport === 'object' && routeExport.router && typeof routeExport.router === 'function' && routeExport.router.stack) {
+                    app.use(`/api/${routeName}`, routeExport.router);
+                    logger.info(`✓ 挂载路由对象: /api/${routeName}`);
+                }
+                // 处理直接导出 routerInstance 的情况 (更严格的检查)
+                else if (typeof routeExport === 'function' && routeExport.handle && routeExport.stack) {
+                    app.use(`/api/${routeName}`, routeExport);
+                    logger.info(`✓ 挂载路由: /api/${routeName}`);
+                } 
+                // 处理导出 { setup: setupFunction } 的情况
+                else if (routeExport && typeof routeExport === 'object' && routeExport.setup && typeof routeExport.setup === 'function') {
+                    routeExport.setup(app);
+                    logger.info(`✓ 初始化路由: ${routeName}`);
+                } 
+                // 处理导出 setup 函数 (app) => {} 的情况
+                else if (typeof routeExport === 'function') {
+                    // 检查是否是 Express Router 实例 (避免重复判断 Case 3)
+                    if (!(routeExport.handle && routeExport.stack)) { 
+                         routeExport(app);
+                         logger.info(`✓ 注册路由函数: ${routeName}`);
+                    } else {
+                         logger.warn(`× 路由 ${file} 格式疑似 Router 实例但未被 Case 3 处理,已跳过`);
+                    }
+                } 
+                // 其他无法识别的格式
+                else {
+                    logger.warn(`× 路由 ${file} 导出格式无法识别 (${typeof routeExport}),已跳过`);
+                }
+            } catch (error) {
+                logger.error(`× 加载路由 ${file} 失败: ${error.message}`);
+                 // 可以在这里添加更详细的错误堆栈日志
+                 logger.debug(error.stack);
+            }
+        });
+        
+        logger.info('所有路由注册完成');
+    } catch (error) {
+        logger.error(`路由注册失败: ${error.message}`);
+    }
+}
+
+module.exports = registerRoutes;

+ 92 - 0
hubcmdui/routes/login.js

@@ -0,0 +1,92 @@
+/**
+ * 登录路由
+ */
+const express = require('express');
+const router = express.Router();
+const fs = require('fs').promises;
+const path = require('path');
+const crypto = require('crypto');
+const logger = require('../logger');
+
+// 生成随机验证码
+function generateCaptcha() {
+    return Math.floor(1000 + Math.random() * 9000).toString();
+}
+
+// 获取验证码
+router.get('/captcha', (req, res) => {
+    const captcha = generateCaptcha();
+    req.session.captcha = captcha;
+    res.json({ captcha });
+});
+
+// 处理登录
+router.post('/login', async (req, res) => {
+    try {
+        const { username, password, captcha } = req.body;
+        
+        // 验证码检查
+        if (!req.session.captcha || req.session.captcha !== captcha) {
+            return res.status(401).json({ error: '验证码错误' });
+        }
+        
+        // 读取用户文件
+        const userFilePath = path.join(__dirname, '../config/users.json');
+        let users;
+        
+        try {
+            const data = await fs.readFile(userFilePath, 'utf8');
+            users = JSON.parse(data);
+        } catch (err) {
+            logger.error('读取用户文件失败:', err);
+            return res.status(500).json({ error: '内部服务器错误' });
+        }
+        
+        // 查找用户
+        const user = users.find(u => u.username === username);
+        if (!user) {
+            return res.status(401).json({ error: '用户名或密码错误' });
+        }
+        
+        // 验证密码
+        const hashedPassword = crypto
+            .createHash('sha256')
+            .update(password + user.salt)
+            .digest('hex');
+            
+        if (hashedPassword !== user.password) {
+            return res.status(401).json({ error: '用户名或密码错误' });
+        }
+        
+        // 登录成功
+        req.session.user = {
+            id: user.id,
+            username: user.username,
+            role: user.role,
+            loginCount: (user.loginCount || 0) + 1,
+            lastLogin: new Date().toISOString()
+        };
+        
+        // 更新登录信息
+        user.loginCount = (user.loginCount || 0) + 1;
+        user.lastLogin = new Date().toISOString();
+        
+        await fs.writeFile(userFilePath, JSON.stringify(users, null, 2), 'utf8');
+        
+        res.json({ 
+            success: true, 
+            user: {
+                username: user.username,
+                role: user.role
+            }
+        });
+    } catch (err) {
+        logger.error('登录处理错误:', err);
+        res.status(500).json({ error: '登录处理失败' });
+    }
+});
+
+logger.success('✓ 登录路由已加载');
+
+// 导出路由
+module.exports = router;

+ 193 - 0
hubcmdui/routes/monitoring.js

@@ -0,0 +1,193 @@
+/**
+ * 监控配置路由
+ */
+const express = require('express');
+const router = express.Router();
+const fs = require('fs').promises;
+const path = require('path');
+const { requireLogin } = require('../middleware/auth');
+const logger = require('../logger');
+
+// 监控配置文件路径
+const CONFIG_FILE = path.join(__dirname, '../config/monitoring.json');
+
+// 确保配置文件存在
+async function ensureConfigFile() {
+    try {
+        await fs.access(CONFIG_FILE);
+    } catch (err) {
+        // 文件不存在,创建默认配置
+        const defaultConfig = {
+            isEnabled: false,
+            notificationType: 'wechat',
+            webhookUrl: '',
+            telegramToken: '',
+            telegramChatId: '',
+            monitorInterval: 60
+        };
+        
+        await fs.mkdir(path.dirname(CONFIG_FILE), { recursive: true });
+        await fs.writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2), 'utf8');
+        return defaultConfig;
+    }
+    
+    // 文件存在,读取配置
+    const data = await fs.readFile(CONFIG_FILE, 'utf8');
+    return JSON.parse(data);
+}
+
+// 获取监控配置
+router.get('/monitoring-config', requireLogin, async (req, res) => {
+    try {
+        const config = await ensureConfigFile();
+        res.json(config);
+    } catch (err) {
+        logger.error('获取监控配置失败:', err);
+        res.status(500).json({ error: '获取监控配置失败' });
+    }
+});
+
+// 保存监控配置
+router.post('/monitoring-config', requireLogin, async (req, res) => {
+    try {
+        const { 
+            notificationType, 
+            webhookUrl, 
+            telegramToken, 
+            telegramChatId, 
+            monitorInterval,
+            isEnabled
+        } = req.body;
+        
+        // 简单验证
+        if (notificationType === 'wechat' && !webhookUrl) {
+            return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
+        }
+        
+        if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+            return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
+        }
+        
+        const config = await ensureConfigFile();
+        
+        // 更新配置
+        const updatedConfig = {
+            ...config,
+            notificationType,
+            webhookUrl: webhookUrl || '',
+            telegramToken: telegramToken || '',
+            telegramChatId: telegramChatId || '',
+            monitorInterval: parseInt(monitorInterval, 10) || 60,
+            isEnabled: isEnabled !== undefined ? isEnabled : config.isEnabled
+        };
+        
+        await fs.writeFile(CONFIG_FILE, JSON.stringify(updatedConfig, null, 2), 'utf8');
+        
+        res.json({ success: true, message: '监控配置已保存' });
+        
+        // 通知监控服务重新加载配置
+        if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
+            global.monitoringService.reload();
+        }
+    } catch (err) {
+        logger.error('保存监控配置失败:', err);
+        res.status(500).json({ error: '保存监控配置失败' });
+    }
+});
+
+// 切换监控状态
+router.post('/toggle-monitoring', requireLogin, async (req, res) => {
+    try {
+        const { isEnabled } = req.body;
+        const config = await ensureConfigFile();
+        
+        config.isEnabled = !!isEnabled;
+        await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
+        
+        res.json({ 
+            success: true, 
+            message: `监控已${isEnabled ? '启用' : '禁用'}`
+        });
+        
+        // 通知监控服务重新加载配置
+        if (global.monitoringService && typeof global.monitoringService.reload === 'function') {
+            global.monitoringService.reload();
+        }
+    } catch (err) {
+        logger.error('切换监控状态失败:', err);
+        res.status(500).json({ error: '切换监控状态失败' });
+    }
+});
+
+// 测试通知
+router.post('/test-notification', requireLogin, async (req, res) => {
+    try {
+        const { 
+            notificationType, 
+            webhookUrl, 
+            telegramToken, 
+            telegramChatId
+        } = req.body;
+        
+        // 简单验证
+        if (notificationType === 'wechat' && !webhookUrl) {
+            return res.status(400).json({ error: '企业微信通知需要设置 webhook URL' });
+        }
+        
+        if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+            return res.status(400).json({ error: 'Telegram 通知需要设置 Token 和 Chat ID' });
+        }
+        
+        // 发送测试通知
+        const notifier = require('../services/notificationService');
+        const testMessage = {
+            title: '测试通知',
+            content: '这是一条测试通知,如果您收到这条消息,说明您的通知配置工作正常。',
+            time: new Date().toLocaleString()
+        };
+        
+        await notifier.sendNotification(testMessage, {
+            type: notificationType,
+            webhookUrl,
+            telegramToken,
+            telegramChatId
+        });
+        
+        res.json({ success: true, message: '测试通知已发送' });
+    } catch (err) {
+        logger.error('发送测试通知失败:', err);
+        res.status(500).json({ error: '发送测试通知失败: ' + err.message });
+    }
+});
+
+// 获取已停止的容器
+router.get('/stopped-containers', async (req, res) => {
+    try {
+        const { exec } = require('child_process');
+        const util = require('util');
+        const execPromise = util.promisify(exec);
+        
+        const { stdout } = await execPromise('docker ps -f "status=exited" --format "{{.ID}}\\t{{.Names}}\\t{{.Status}}"');
+        
+        const containers = stdout.trim().split('\n')
+            .filter(line => line.trim())
+            .map(line => {
+                const [id, name, ...statusParts] = line.split('\t');
+                return {
+                    id: id.substring(0, 12),
+                    name,
+                    status: statusParts.join(' ')
+                };
+            });
+        
+        res.json(containers);
+    } catch (err) {
+        logger.error('获取已停止容器失败:', err);
+        res.status(500).json({ error: '获取已停止容器失败', details: err.message });
+    }
+});
+
+logger.success('✓ 监控配置路由已加载');
+
+// 导出路由
+module.exports = router;

+ 55 - 0
hubcmdui/routes/routeLoader.js

@@ -0,0 +1,55 @@
+const fs = require('fs');
+const path = require('path');
+const express = require('express');
+const { executeOnce } = require('../lib/initScheduler');
+
+// 引入logger
+const logger = require('../logger');
+
+// 改进路由加载器,确保每个路由只被加载一次
+async function loadRoutes(app, customLogger) {
+    // 使用传入的logger或默认logger
+    const log = customLogger || logger;
+    
+    const routesDir = path.join(__dirname);
+    const routeFiles = fs.readdirSync(routesDir).filter(file => 
+        file.endsWith('.js') && !file.includes('routeLoader') && !file.includes('index')
+    );
+    
+    log.info(`发现 ${routeFiles.length} 个路由文件待加载`);
+    
+    for (const file of routeFiles) {
+        const routeName = path.basename(file, '.js');
+        
+        try {
+            await executeOnce(`loadRoute_${routeName}`, async () => {
+                const routePath = path.join(routesDir, file);
+                
+                // 添加错误处理来避免路由加载失败时导致应用崩溃
+                try {
+                    const route = require(routePath);
+                    
+                    if (typeof route === 'function') {
+                        route(app);
+                        log.success(`✓ 注册路由: ${routeName}`);
+                    } else if (route && typeof route.router === 'function') {
+                        route.router(app);
+                        log.success(`✓ 注册路由对象: ${routeName}`);
+                    } else {
+                        log.error(`× 路由格式错误: ${file} (应该导出一个函数或router方法)`);
+                    }
+                } catch (routeError) {
+                    log.error(`× 加载路由 ${file} 失败: ${routeError.message}`);
+                    // 继续加载其他路由,不中断流程
+                }
+            }, log);
+        } catch (error) {
+            log.error(`× 路由加载流程出错: ${error.message}`);
+            // 继续处理下一个路由
+        }
+    }
+    
+    log.info('所有路由注册完成');
+}
+
+module.exports = loadRoutes;

+ 590 - 0
hubcmdui/routes/system.js

@@ -0,0 +1,590 @@
+/**
+ * 系统相关路由
+ */
+const express = require('express');
+const router = express.Router();
+const os = require('os'); // 确保导入 os 模块
+const util = require('util'); // 导入 util 模块
+const { exec } = require('child_process');
+const execPromise = util.promisify(exec); // 只在这里定义一次
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+const configService = require('../services/configService');
+const { execCommand, getSystemInfo } = require('../server-utils');
+const dockerService = require('../services/dockerService');
+const path = require('path');
+const fs = require('fs').promises;
+
+// 获取系统状态
+async function getSystemStats(req, res) {
+  try {
+    let dockerAvailable = false;
+    let containerCount = '0';
+    let memoryUsage = '0%';
+    let cpuLoad = '0%';
+    let diskSpace = '0%';
+    let recentActivities = [];
+
+    // 尝试获取系统信息
+    try {
+      const systemInfo = await getSystemInfo();
+      memoryUsage = `${systemInfo.memory.percent}%`;
+      cpuLoad = systemInfo.cpu.load1;
+      diskSpace = systemInfo.disk.percent;
+    } catch (sysError) {
+      logger.error('获取系统信息失败:', sysError);
+    }
+
+    // 尝试从Docker获取状态信息
+    try {
+      const docker = await dockerService.getDockerConnection();
+      if (docker) {
+        dockerAvailable = true;
+        
+        // 获取容器统计
+        const containers = await docker.listContainers({ all: true });
+        containerCount = containers.length.toString();
+        
+        // 获取最近的容器活动
+        const runningContainers = containers.filter(c => c.State === 'running');
+        for (let i = 0; i < Math.min(3, runningContainers.length); i++) {
+          recentActivities.push({
+            time: new Date(runningContainers[i].Created * 1000).toLocaleString(),
+            action: '运行中',
+            container: runningContainers[i].Names[0].replace(/^\//, ''),
+            status: '正常'
+          });
+        }
+
+        // 获取最近的Docker事件
+        const events = await dockerService.getRecentEvents();
+        if (events && events.length > 0) {
+          recentActivities = [...events.map(event => ({
+            time: new Date(event.time * 1000).toLocaleString(),
+            action: event.Action,
+            container: event.Actor?.Attributes?.name || '未知容器',
+            status: event.status || '完成'
+          })), ...recentActivities].slice(0, 10);
+        }
+      }
+    } catch (containerError) {
+      logger.error('获取容器信息失败:', containerError);
+    }
+
+    // 如果没有活动记录,添加一个默认记录
+    if (recentActivities.length === 0) {
+      recentActivities.push({
+        time: new Date().toLocaleString(),
+        action: '系统检查',
+        container: '监控服务',
+        status: dockerAvailable ? '正常' : 'Docker服务不可用'
+      });
+    }
+
+    // 返回收集到的所有数据,即使部分数据可能不完整
+    res.json({
+      dockerAvailable,
+      containerCount,
+      memoryUsage,
+      cpuLoad,
+      diskSpace,
+      recentActivities
+    });
+  } catch (error) {
+    logger.error('获取系统统计数据失败:', error);
+    
+    // 即使出错,仍然尝试返回一些基本数据
+    res.status(200).json({
+      dockerAvailable: false,
+      containerCount: '0',
+      memoryUsage: '未知',
+      cpuLoad: '未知',
+      diskSpace: '未知',
+      recentActivities: [{
+        time: new Date().toLocaleString(),
+        action: '系统错误',
+        container: '监控服务',
+        status: '数据获取失败'
+      }],
+      error: '获取系统统计数据失败',
+      errorDetails: error.message
+    });
+  }
+}
+
+// 获取系统配置 - 修改版本,避免与其他路由冲突
+router.get('/system-config', async (req, res) => {
+  try {
+    const config = await configService.getConfig();
+    res.json(config);
+  } catch (error) {
+    logger.error('读取配置失败:', error);
+    res.status(500).json({ 
+      error: '读取配置失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 保存系统配置 - 修改版本,避免与其他路由冲突
+router.post('/system-config', requireLogin, async (req, res) => {
+  try {
+    const currentConfig = await configService.getConfig();
+    const newConfig = { ...currentConfig, ...req.body };
+    await configService.saveConfig(newConfig);
+    logger.info('系统配置已更新');
+    res.json({ success: true });
+  } catch (error) {
+    logger.error('保存配置失败:', error);
+    res.status(500).json({ 
+      error: '保存配置失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 获取系统状态
+router.get('/stats', requireLogin, async (req, res) => {
+  return await getSystemStats(req, res);
+});
+
+// 获取磁盘空间信息
+router.get('/disk-space', requireLogin, async (req, res) => {
+  try {
+    const systemInfo = await getSystemInfo();
+    res.json({
+      diskSpace: `${systemInfo.disk.used}/${systemInfo.disk.size}`,
+      usagePercent: parseInt(systemInfo.disk.percent)
+    });
+  } catch (error) {
+    logger.error('获取磁盘空间信息失败:', error);
+    res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message });
+  }
+});
+
+// 网络测试
+router.post('/network-test', requireLogin, async (req, res) => {
+  const { type, domain } = req.body;
+  
+  // 验证输入
+  function validateInput(input, type) {
+    if (type === 'domain') {
+      return /^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(input);
+    }
+    return false;
+  }
+  
+  if (!validateInput(domain, 'domain')) {
+    return res.status(400).json({ error: '无效的域名格式' });
+  }
+  
+  try {
+    const result = await execCommand(`${type === 'ping' ? 'ping -c 4' : 'traceroute -m 10'} ${domain}`, { timeout: 30000 });
+    res.send(result);
+  } catch (error) {
+    if (error.killed) {
+      return res.status(408).send('测试超时');
+    }
+    logger.error(`执行网络测试命令错误:`, error);
+    res.status(500).send('测试执行失败: ' + error.message);
+  }
+});
+
+// 获取用户统计信息
+router.get('/user-stats', requireLogin, async (req, res) => {
+  try {
+    const userService = require('../services/userService');
+    const username = req.session.user.username;
+    const userStats = await userService.getUserStats(username);
+    
+    res.json(userStats);
+  } catch (error) {
+    logger.error('获取用户统计信息失败:', error);
+    res.status(500).json({
+      loginCount: '0',
+      lastLogin: '未知',
+      accountAge: '0'
+    });
+  }
+});
+
+// 获取系统状态信息 (旧版,可能与 getSystemStats 重复,可以考虑移除)
+router.get('/system-status', requireLogin, async (req, res) => {
+    logger.warn('Accessing potentially deprecated /api/system-status route.');
+    try {
+        // 检查 Docker 可用性
+        let dockerAvailable = true;
+        let containerCount = 0;
+        try {
+            // 避免直接执行命令计算,依赖 dockerService
+            const docker = await dockerService.getDockerConnection();
+            if (docker) {
+                 const containers = await docker.listContainers({ all: true });
+                 containerCount = containers.length;
+            } else {
+                 dockerAvailable = false;
+            }
+        } catch (dockerError) {
+            dockerAvailable = false;
+            containerCount = 0;
+            logger.warn('Docker可能未运行或无法访问 (in /system-status):', dockerError.message);
+        }
+        
+        // 获取内存使用信息
+        const totalMem = os.totalmem();
+        const freeMem = os.freemem();
+        const usedMem = totalMem - freeMem;
+        const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`;
+        
+        // 获取CPU负载
+        const [load1] = os.loadavg();
+        const cpuCount = os.cpus().length || 1; // 避免除以0
+        const cpuLoad = `${(load1 / cpuCount * 100).toFixed(1)}%`;
+        
+        // 获取磁盘空间 - 简单版
+        let diskSpace = '未知';
+        try {
+            if (os.platform() === 'darwin' || os.platform() === 'linux') {
+                 const { stdout } = await execPromise('df -h / | tail -n 1'); // 使用 -n 1
+                 const parts = stdout.trim().split(/\s+/);
+                 if (parts.length >= 5) diskSpace = parts[4];
+            } else if (os.platform() === 'win32') {
+                 const { stdout } = await execPromise('wmic logicaldisk get size,freespace,caption | findstr /B /L /V "Caption" ');
+                 const lines = stdout.trim().split(/\r?\n/);
+                 if (lines.length > 0) {
+                      const parts = lines[0].trim().split(/\s+/);
+                      if (parts.length >= 2) {
+                           const free = parseInt(parts[0], 10);
+                           const total = parseInt(parts[1], 10);
+                           if (!isNaN(total) && !isNaN(free) && total > 0) {
+                                diskSpace = `${Math.round(((total - free) / total) * 100)}%`;
+                           }
+                      }
+                 }
+            }
+        } catch (diskError) {
+            logger.warn('获取磁盘空间失败 (in /system-status):', diskError.message);
+            diskSpace = '未知';
+        }
+
+        // 格式化系统运行时间
+        const uptime = formatUptime(os.uptime());
+
+        res.json({
+            dockerAvailable,
+            containerCount,
+            memoryUsage,
+            cpuLoad,
+            diskSpace,
+            systemUptime: uptime
+        });
+    } catch (error) {
+        logger.error('获取系统状态失败 (in /system-status):', error);
+        res.status(500).json({ 
+            error: '获取系统状态失败', 
+            message: error.message
+        });
+    }
+});
+
+// 添加新的API端点,提供完整系统资源信息
+router.get('/system-resources', requireLogin, async (req, res) => {
+    logger.info('Received request for /api/system-resources');
+    let cpuInfoData = null, memoryData = null, diskInfoData = null, systemData = null;
+    
+    // --- 获取 CPU 信息 (独立 try...catch) ---
+    try {
+         const cpuInfo = os.cpus();
+         const [load1, load5, load15] = os.loadavg();
+         const cpuCount = cpuInfo.length || 1;
+         const cpuUsage = (load1 / cpuCount * 100).toFixed(1);
+         cpuInfoData = {
+             cores: cpuCount,
+             model: cpuInfo[0]?.model || '未知',
+             speed: `${cpuInfo[0]?.speed || '未知'} MHz`,
+             loadAvg: {
+                 '1min': load1.toFixed(2),
+                 '5min': load5.toFixed(2),
+                 '15min': load15.toFixed(2)
+             },
+             usage: parseFloat(cpuUsage)
+         };
+         logger.info('Successfully retrieved CPU info.');
+    } catch (cpuError) {
+         logger.error('Error getting CPU info:', cpuError.message);
+         cpuInfoData = { error: '获取 CPU 信息失败', message: cpuError.message }; // 返回错误信息
+    }
+    
+    // --- 获取内存信息 (独立 try...catch) ---
+    try {
+         const totalMem = os.totalmem();
+         const freeMem = os.freemem();
+         const usedMem = totalMem - freeMem;
+         const memoryUsagePercent = totalMem > 0 ? Math.round(usedMem / totalMem * 100) : 0;
+         memoryData = {
+             total: formatBytes(totalMem), // 可能出错
+             free: formatBytes(freeMem), // 可能出错
+             used: formatBytes(usedMem), // 可能出错
+             usedPercentage: memoryUsagePercent
+         };
+          logger.info('Successfully retrieved Memory info.');
+    } catch (memError) {
+         logger.error('Error getting Memory info:', memError.message);
+         memoryData = { error: '获取内存信息失败', message: memError.message }; // 返回错误信息
+    }
+    
+    // --- 获取磁盘信息 (独立 try...catch) ---
+    try {
+        let diskResult = { total: '未知', free: '未知', used: '未知', usedPercentage: '未知' }; 
+        logger.info(`Getting disk info for platform: ${os.platform()}`);
+        if (os.platform() === 'darwin' || os.platform() === 'linux') {
+            try {
+                 // 使用 -k 获取 KB 单位,方便计算
+                 const { stdout } = await execPromise('df -k / | tail -n 1', { timeout: 5000 }); 
+                 logger.info(`'df -k' command output: ${stdout}`);
+                 const parts = stdout.trim().split(/\s+/);
+                 // 索引通常是 1=Total, 2=Used, 3=Available, 4=Use%
+                 if (parts.length >= 4) { 
+                     const total = parseInt(parts[1], 10) * 1024; // KB to Bytes
+                     const used = parseInt(parts[2], 10) * 1024;  // KB to Bytes
+                     const free = parseInt(parts[3], 10) * 1024;  // KB to Bytes
+                     // 优先使用命令输出的百分比,更准确
+                     let usedPercentage = parseInt(parts[4].replace('%', ''), 10);
+                     
+                     // 如果解析失败或百分比无效,则尝试计算
+                     if (isNaN(usedPercentage) && !isNaN(total) && !isNaN(used) && total > 0) {
+                         usedPercentage = Math.round((used / total) * 100);
+                     }
+
+                     if (!isNaN(total) && !isNaN(used) && !isNaN(free) && !isNaN(usedPercentage)) {
+                          diskResult = {
+                              total: formatBytes(total), // 可能出错
+                              free: formatBytes(free), // 可能出错
+                              used: formatBytes(used), // 可能出错
+                              usedPercentage: usedPercentage
+                          };
+                          logger.info('Successfully parsed disk info (Linux/Darwin).');
+                     } else {
+                          logger.warn('Failed to parse numbers from df output:', parts);
+                          diskResult = { ...diskResult, error: '解析 df 输出失败' }; // 添加错误标记
+                     }
+                 } else {
+                      logger.warn('Unexpected output format from df:', stdout);
+                      diskResult = { ...diskResult, error: 'df 输出格式不符合预期' }; // 添加错误标记
+                 }
+            } catch (dfError) {
+                logger.error(`Error executing or parsing 'df -k': ${dfError.message}`);
+                if (dfError.killed) logger.error("'df -k' command timed out."); 
+                diskResult = { error: '获取磁盘信息失败 (df)', message: dfError.message }; // 标记错误
+            }
+        } else if (os.platform() === 'win32') {
+            try {
+                 // 获取 C 盘信息 (可以修改为获取所有盘符或特定盘符)
+                 const { stdout } = await execPromise(`wmic logicaldisk where "DeviceID='C:'" get size,freespace /value`, { timeout: 5000 });
+                 logger.info(`'wmic' command output: ${stdout}`);
+                 const lines = stdout.trim().split(/\r?\n/);
+                 let free = NaN, total = NaN;
+                 lines.forEach(line => {
+                     if (line.startsWith('FreeSpace=')) {
+                         free = parseInt(line.split('=')[1], 10);
+                     } else if (line.startsWith('Size=')) {
+                         total = parseInt(line.split('=')[1], 10);
+                     }
+                 });
+
+                 if (!isNaN(total) && !isNaN(free) && total > 0) {
+                     const used = total - free;
+                     const usedPercentage = Math.round((used / total) * 100);
+                     diskResult = {
+                         total: formatBytes(total), // 可能出错
+                         free: formatBytes(free), // 可能出错
+                         used: formatBytes(used), // 可能出错
+                         usedPercentage: usedPercentage
+                     };
+                     logger.info('Successfully parsed disk info (Windows - C:).');
+                 } else {
+                      logger.warn('Failed to parse numbers from wmic output:', stdout);
+                      diskResult = { ...diskResult, error: '解析 wmic 输出失败' }; // 添加错误标记
+                 }
+            } catch (wmicError) {
+                 logger.error(`Error executing or parsing 'wmic': ${wmicError.message}`);
+                 if (wmicError.killed) logger.error("'wmic' command timed out.");
+                 diskResult = { error: '获取磁盘信息失败 (wmic)', message: wmicError.message }; // 标记错误
+            }
+        }
+        diskInfoData = diskResult; 
+    } catch (diskErrorOuter) {
+        logger.error('Unexpected error during disk info gathering:', diskErrorOuter.message);
+        diskInfoData = { error: '获取磁盘信息时发生意外错误', message: diskErrorOuter.message }; // 返回错误信息
+    }
+    
+    // --- 获取其他系统信息 (独立 try...catch) ---
+     try {
+          systemData = {
+              platform: os.platform(),
+              release: os.release(),
+              hostname: os.hostname(),
+              uptime: formatUptime(os.uptime()) // 可能出错
+          };
+          logger.info('Successfully retrieved general system info.');
+     } catch (sysInfoError) {
+          logger.error('Error getting general system info:', sysInfoError.message);
+          systemData = { error: '获取常规系统信息失败', message: sysInfoError.message }; // 返回错误信息
+     }
+
+    // --- 包装 Helper 函数调用以捕获潜在错误 ---
+    const safeFormatBytes = (bytes) => {
+        try {
+            return formatBytes(bytes);
+        } catch (e) {
+            logger.error(`formatBytes failed for value ${bytes}:`, e.message);
+            return '格式化错误';
+        }
+    };
+    const safeFormatUptime = (seconds) => {
+        try {
+            return formatUptime(seconds);
+        } catch (e) {
+            logger.error(`formatUptime failed for value ${seconds}:`, e.message);
+            return '格式化错误';
+        }
+    };
+
+    // --- 构建最终响应数据,使用安全的 Helper 函数 --- 
+    const finalCpuData = cpuInfoData?.error ? cpuInfoData : {
+        ...cpuInfoData
+        // CPU 不需要格式化
+    };
+    const finalMemoryData = memoryData?.error ? memoryData : {
+        ...memoryData,
+        total: safeFormatBytes(os.totalmem()),
+        free: safeFormatBytes(os.freemem()),
+        used: safeFormatBytes(os.totalmem() - os.freemem())
+    };
+    const finalDiskData = diskInfoData?.error ? diskInfoData : {
+        ...diskInfoData,
+        // 如果 diskInfoData 内部有 total/free/used (字节数),则格式化
+        // 否则保持 '未知' 或已格式化的字符串
+        total: (diskInfoData?.total && typeof diskInfoData.total === 'number') ? safeFormatBytes(diskInfoData.total) : diskInfoData?.total || '未知',
+        free: (diskInfoData?.free && typeof diskInfoData.free === 'number') ? safeFormatBytes(diskInfoData.free) : diskInfoData?.free || '未知',
+        used: (diskInfoData?.used && typeof diskInfoData.used === 'number') ? safeFormatBytes(diskInfoData.used) : diskInfoData?.used || '未知'
+    };
+    const finalSystemData = systemData?.error ? systemData : {
+        ...systemData,
+        uptime: safeFormatUptime(os.uptime())
+    };
+
+    const responseData = {
+        cpu: finalCpuData,
+        memory: finalMemoryData,
+        diskSpace: finalDiskData,
+        system: finalSystemData
+    };
+    
+    logger.info('Sending response for /api/system-resources:', JSON.stringify(responseData));
+    res.status(200).json(responseData); 
+});
+
+// 格式化系统运行时间
+function formatUptime(seconds) {
+    const days = Math.floor(seconds / 86400);
+    seconds %= 86400;
+    const hours = Math.floor(seconds / 3600);
+    seconds %= 3600;
+    const minutes = Math.floor(seconds / 60);
+    seconds = Math.floor(seconds % 60);
+    
+    let result = '';
+    if (days > 0) result += `${days}天 `;
+    if (hours > 0 || days > 0) result += `${hours}小时 `;
+    if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}分钟 `;
+    result += `${seconds}秒`;
+    
+    return result;
+}
+
+// 获取系统资源详情
+router.get('/system-resource-details', requireLogin, async (req, res) => {
+    try {
+        const { type } = req.query;
+        
+        let data = {};
+        
+        switch (type) {
+            case 'memory':
+                const totalMem = os.totalmem();
+                const freeMem = os.freemem();
+                const usedMem = totalMem - freeMem;
+                
+                data = {
+                    totalMemory: formatBytes(totalMem),
+                    usedMemory: formatBytes(usedMem),
+                    freeMemory: formatBytes(freeMem),
+                    memoryUsage: `${Math.round(usedMem / totalMem * 100)}%`
+                };
+                break;
+                
+            case 'cpu':
+                const cpuInfo = os.cpus();
+                const [load1, load5, load15] = os.loadavg();
+                
+                data = {
+                    cpuCores: cpuInfo.length,
+                    cpuModel: cpuInfo[0].model,
+                    cpuSpeed: `${cpuInfo[0].speed} MHz`,
+                    loadAvg1: load1.toFixed(2),
+                    loadAvg5: load5.toFixed(2),
+                    loadAvg15: load15.toFixed(2),
+                    cpuLoad: `${(load1 / cpuInfo.length * 100).toFixed(1)}%`
+                };
+                break;
+                
+            case 'disk':
+                try {
+                    const { stdout: dfOutput } = await execPromise('df -h / | tail -n 1');
+                    const parts = dfOutput.trim().split(/\s+/);
+                    
+                    if (parts.length >= 5) {
+                        data = {
+                            totalSpace: parts[1],
+                            usedSpace: parts[2],
+                            freeSpace: parts[3],
+                            diskUsage: parts[4]
+                        };
+                    } else {
+                        throw new Error('解析磁盘信息失败');
+                    }
+                } catch (diskError) {
+                    logger.warn('获取磁盘信息失败:', diskError.message);
+                    data = {
+                        error: '获取磁盘信息失败',
+                        message: diskError.message
+                    };
+                }
+                break;
+                
+            default:
+                return res.status(400).json({ error: '无效的资源类型' });
+        }
+        
+        res.json(data);
+    } catch (error) {
+        logger.error('获取系统资源详情失败:', error);
+        res.status(500).json({ error: '获取系统资源详情失败', message: error.message });
+    }
+});
+
+// 格式化字节数为可读格式
+function formatBytes(bytes, decimals = 2) {
+    if (bytes === 0) return '0 Bytes';
+    
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+    
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+}
+
+module.exports = router; // 只导出 router

+ 104 - 0
hubcmdui/routes/systemStatus.js

@@ -0,0 +1,104 @@
+const express = require('express');
+const router = express.Router();
+const os = require('os');
+const logger = require('../logger');
+
+// 获取系统状态
+router.get('/', (req, res) => {
+    try {
+        // 收集系统信息
+        const cpuLoad = os.loadavg()[0] / os.cpus().length * 100;
+        const totalMem = os.totalmem();
+        const freeMem = os.freemem();
+        const usedMem = totalMem - freeMem;
+        const memoryUsage = `${Math.round(usedMem / totalMem * 100)}%`;
+        
+        // 组合结果
+        const systemStatus = {
+            dockerAvailable: true,
+            containerCount: 0,
+            cpuLoad: `${cpuLoad.toFixed(1)}%`,
+            memoryUsage: memoryUsage,
+            diskSpace: '未知',
+            recentActivities: []
+        };
+        
+        res.json(systemStatus);
+    } catch (error) {
+        logger.error('获取系统状态失败:', error);
+        res.status(500).json({ 
+            error: '获取系统状态失败',
+            details: error.message 
+        });
+    }
+});
+
+// 获取系统资源详情
+router.get('/system-resource-details', (req, res) => {
+    try {
+        const { type } = req.query;
+        let data = {};
+        
+        switch(type) {
+            case 'memory':
+                const totalMem = os.totalmem();
+                const freeMem = os.freemem();
+                const usedMem = totalMem - freeMem;
+                
+                data = {
+                    totalMemory: formatBytes(totalMem),
+                    usedMemory: formatBytes(usedMem),
+                    freeMemory: formatBytes(freeMem),
+                    memoryUsage: `${Math.round(usedMem / totalMem * 100)}%`
+                };
+                break;
+                
+            case 'cpu':
+                const cpus = os.cpus();
+                const loadAvg = os.loadavg();
+                
+                data = {
+                    cpuCores: cpus.length,
+                    cpuModel: cpus[0].model,
+                    cpuLoad: `${(loadAvg[0] / cpus.length * 100).toFixed(1)}%`,
+                    loadAvg1: loadAvg[0].toFixed(2),
+                    loadAvg5: loadAvg[1].toFixed(2),
+                    loadAvg15: loadAvg[2].toFixed(2)
+                };
+                break;
+                
+            case 'disk':
+                // 简单的硬编码数据,在实际环境中应该调用系统命令获取
+                data = {
+                    totalSpace: '100 GB',
+                    usedSpace: '30 GB',
+                    freeSpace: '70 GB',
+                    diskUsage: '30%'
+                };
+                break;
+                
+            default:
+                return res.status(400).json({ error: '无效的资源类型' });
+        }
+        
+        res.json(data);
+    } catch (error) {
+        logger.error('获取系统资源详情失败:', error);
+        res.status(500).json({ error: '获取系统资源详情失败', details: error.message });
+    }
+});
+
+// 格式化字节数为可读格式
+function formatBytes(bytes, decimals = 2) {
+    if (bytes === 0) return '0 Bytes';
+    
+    const k = 1024;
+    const dm = decimals < 0 ? 0 : decimals;
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+    
+    const i = Math.floor(Math.log(bytes) / Math.log(k));
+    
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+}
+
+module.exports = router;

+ 163 - 0
hubcmdui/scripts/diagnostics.js

@@ -0,0 +1,163 @@
+/**
+ * 系统诊断工具 - 帮助找出可能存在的问题
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const { execSync } = require('child_process');
+const logger = require('../logger');
+
+// 检查所有必要的文件和目录是否存在
+async function checkFilesAndDirectories() {
+  logger.info('开始检查必要的文件和目录...');
+  
+  // 检查必要的目录
+  const requiredDirs = [
+    { path: 'logs', critical: true },
+    { path: 'documentation', critical: true },
+    { path: 'web/images', critical: true },
+    { path: 'routes', critical: true },
+    { path: 'services', critical: true },
+    { path: 'middleware', critical: true },
+    { path: 'scripts', critical: false }
+  ];
+  
+  const dirsStatus = {};
+  for (const dir of requiredDirs) {
+    const fullPath = path.join(__dirname, '..', dir.path);
+    try {
+      await fs.access(fullPath);
+      dirsStatus[dir.path] = { exists: true, critical: dir.critical };
+      logger.info(`目录存在: ${dir.path}`);
+    } catch (error) {
+      dirsStatus[dir.path] = { exists: false, critical: dir.critical };
+      logger.error(`目录不存在: ${dir.path} (${dir.critical ? '关键' : '非关键'})`);
+    }
+  }
+  
+  // 检查必要的文件
+  const requiredFiles = [
+    { path: 'server.js', critical: true },
+    { path: 'app.js', critical: false },
+    { path: 'config.js', critical: true },
+    { path: 'logger.js', critical: true },
+    { path: 'init-dirs.js', critical: true },
+    { path: 'download-images.js', critical: true },
+    { path: 'cleanup.js', critical: true },
+    { path: 'package.json', critical: true },
+    { path: 'web/index.html', critical: true },
+    { path: 'web/admin.html', critical: true }
+  ];
+  
+  const filesStatus = {};
+  for (const file of requiredFiles) {
+    const fullPath = path.join(__dirname, '..', file.path);
+    try {
+      await fs.access(fullPath);
+      filesStatus[file.path] = { exists: true, critical: file.critical };
+      logger.info(`文件存在: ${file.path}`);
+    } catch (error) {
+      filesStatus[file.path] = { exists: false, critical: file.critical };
+      logger.error(`文件不存在: ${file.path} (${file.critical ? '关键' : '非关键'})`);
+    }
+  }
+  
+  return { directories: dirsStatus, files: filesStatus };
+}
+
+// 检查Node.js模块依赖
+function checkNodeDependencies() {
+  logger.info('开始检查Node.js依赖...');
+  
+  try {
+    // 执行npm list --depth=0来检查已安装的依赖
+    const npmListOutput = execSync('npm list --depth=0', { encoding: 'utf8' });
+    logger.info('已安装的依赖:\n' + npmListOutput);
+    
+    return { success: true, output: npmListOutput };
+  } catch (error) {
+    logger.error('检查依赖时出错:', error.message);
+    return { success: false, error: error.message };
+  }
+}
+
+// 检查系统环境
+async function checkSystemEnvironment() {
+  logger.info('开始检查系统环境...');
+  
+  const checks = {
+    node: process.version,
+    platform: process.platform,
+    arch: process.arch,
+    docker: null
+  };
+  
+  try {
+    // 检查Docker是否可用
+    const dockerVersion = execSync('docker --version', { encoding: 'utf8' });
+    checks.docker = dockerVersion.trim();
+    logger.info(`Docker版本: ${dockerVersion.trim()}`);
+  } catch (error) {
+    checks.docker = false;
+    logger.warn('Docker未安装或不可用');
+  }
+  
+  return checks;
+}
+
+// 运行诊断
+async function runDiagnostics() {
+  logger.info('======= 开始系统诊断 =======');
+  
+  const results = {
+    filesAndDirs: await checkFilesAndDirectories(),
+    dependencies: checkNodeDependencies(),
+    environment: await checkSystemEnvironment()
+  };
+  
+  // 检查关键错误
+  const criticalErrors = [];
+  
+  // 检查关键目录
+  Object.entries(results.filesAndDirs.directories).forEach(([dir, status]) => {
+    if (status.critical && !status.exists) {
+      criticalErrors.push(`关键目录丢失: ${dir}`);
+    }
+  });
+  
+  // 检查关键文件
+  Object.entries(results.filesAndDirs.files).forEach(([file, status]) => {
+    if (status.critical && !status.exists) {
+      criticalErrors.push(`关键文件丢失: ${file}`);
+    }
+  });
+  
+  // 检查依赖
+  if (!results.dependencies.success) {
+    criticalErrors.push('依赖检查失败');
+  }
+  
+  // 总结
+  logger.info('======= 诊断完成 =======');
+  if (criticalErrors.length > 0) {
+    logger.error('发现关键错误:');
+    criticalErrors.forEach(err => logger.error(`- ${err}`));
+    logger.error('请解决以上问题后重试');
+  } else {
+    logger.success('未发现关键错误,系统应该可以正常运行');
+  }
+  
+  return { results, criticalErrors };
+}
+
+// 直接运行脚本时启动诊断
+if (require.main === module) {
+  runDiagnostics()
+    .then(() => {
+      logger.info('诊断完成');
+    })
+    .catch(error => {
+      logger.fatal('诊断过程中发生错误:', error);
+    });
+}
+
+module.exports = { runDiagnostics };

+ 25 - 0
hubcmdui/scripts/init-menu.js

@@ -0,0 +1,25 @@
+const MenuItem = require('../models/MenuItem');
+
+async function initMenuItems() {
+    const count = await MenuItem.countDocuments();
+    if (count === 0) {
+        await MenuItem.insertMany([
+            {
+                text: '首页',
+                link: '/',
+                icon: 'fa-home',
+                order: 1
+            },
+            {
+                text: '文档',
+                link: '/docs',
+                icon: 'fa-book',
+                order: 2
+            }
+            // 添加更多默认菜单项...
+        ]);
+        console.log('默认菜单项已初始化');
+    }
+}
+
+module.exports = initMenuItems;

+ 315 - 0
hubcmdui/scripts/init-system.js

@@ -0,0 +1,315 @@
+/**
+ * 系统初始化脚本 - 首次运行时执行
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const bcrypt = require('bcrypt');
+const { execSync } = require('child_process');
+const logger = require('../logger');
+const { ensureDirectoriesExist } = require('../init-dirs');
+const { downloadImages } = require('../download-images');
+const configService = require('../services/configService');
+
+// 用户文件路径
+const USERS_FILE = path.join(__dirname, '..', 'users.json');
+
+/**
+ * 创建管理员用户
+ * @param {string} username 用户名
+ * @param {string} password 密码
+ */
+async function createAdminUser(username = 'root', password = 'admin') {
+  try {
+    // 检查用户文件是否已存在
+    try {
+      await fs.access(USERS_FILE);
+      logger.info('用户文件已存在,跳过创建管理员用户');
+      return;
+    } catch (err) {
+      if (err.code !== 'ENOENT') throw err;
+    }
+    
+    // 创建默认管理员用户
+    const defaultUser = {
+      username,
+      password: bcrypt.hashSync(password, 10),
+      createdAt: new Date().toISOString(),
+      loginCount: 0,
+      lastLogin: null
+    };
+    
+    await fs.writeFile(USERS_FILE, JSON.stringify({ users: [defaultUser] }, null, 2));
+    logger.success(`创建默认管理员用户: ${username}/${password}`);
+    logger.warn('请在首次登录后立即修改默认密码');
+  } catch (error) {
+    logger.error('创建管理员用户失败:', error);
+    throw error;
+  }
+}
+
+/**
+ * 创建默认配置
+ */
+async function createDefaultConfig() {
+  try {
+    // 检查配置是否已存在
+    const config = await configService.getConfig();
+    
+    // 如果菜单项为空,添加默认菜单项
+    if (!config.menuItems || config.menuItems.length === 0) {
+      config.menuItems = [
+        {
+          text: "控制台",
+          link: "/admin",
+          newTab: false
+        },
+        {
+          text: "镜像搜索",
+          link: "/",
+          newTab: false
+        },
+        {
+          text: "文档",
+          link: "/docs",
+          newTab: false
+        },
+        {
+          text: "GitHub",
+          link: "https://github.com/dqzboy/hubcmdui",
+          newTab: true
+        }
+      ];
+      
+      await configService.saveConfig(config);
+      logger.success('创建默认菜单配置');
+    }
+    
+    return config;
+  } catch (error) {
+    logger.error('初始化配置失败:', error);
+    throw error;
+  }
+}
+
+/**
+ * 创建示例文档 - 现已禁用
+ */
+async function createSampleDocumentation() {
+  logger.info('示例文档创建功能已禁用');
+  return; // 不再创建默认文档
+  
+  /* 旧代码保留注释,已禁用
+  const docService = require('../services/documentationService');
+  
+  try {
+    await docService.ensureDocumentationDir();
+    
+    // 检查是否有现有文档
+    const docs = await docService.getDocumentationList();
+    if (docs && docs.length > 0) {
+      logger.info('文档已存在,跳过创建示例文档');
+      return;
+    }
+    
+    // 创建示例文档
+    const welcomeDoc = {
+      title: "欢迎使用 Docker 镜像代理加速系统",
+      content: `# 欢迎使用 Docker 镜像代理加速系统
+
+## 系统简介
+
+Docker 镜像代理加速系统是一个帮助用户快速搜索、拉取 Docker 镜像的工具。本系统提供了以下功能:
+
+- 快速搜索 Docker Hub 上的镜像
+- 查看镜像的详细信息和标签
+- 管理本地 Docker 容器
+- 监控容器状态并发送通知
+
+## 快速开始
+
+1. 在首页搜索框中输入要查找的镜像名称
+2. 点击搜索结果查看详细信息
+3. 使用提供的命令拉取镜像
+
+## 管理功能
+
+管理员可以通过控制面板管理系统:
+
+- 查看所有容器状态
+- 启动/停止/重启容器
+- 更新容器镜像
+- 配置监控告警
+
+祝您使用愉快!
+`,
+      published: true
+    };
+    
+    const aboutDoc = {
+      title: "关于系统",
+      content: `# 关于 Docker 镜像代理加速系统
+
+## 系统版本
+
+当前版本: v1.0.0
+
+## 技术栈
+
+- 前端: HTML, CSS, JavaScript
+- 后端: Node.js, Express
+- 容器: Docker, Dockerode
+- 数据存储: 文件系统
+
+## 联系方式
+
+如有问题,请通过以下方式联系我们:
+
+- GitHub Issues
+- 电子邮件: [email protected]
+
+## 许可证
+
+本项目采用 MIT 许可证
+`,
+      published: true
+    };
+    
+    await docService.saveDocument(Date.now().toString(), welcomeDoc.title, welcomeDoc.content);
+    await docService.saveDocument((Date.now() + 1000).toString(), aboutDoc.title, aboutDoc.content);
+    
+    logger.success('创建示例文档成功');
+  } catch (error) {
+    logger.error('创建示例文档失败:', error);
+  }
+  */
+}
+
+/**
+ * 检查必要依赖
+ */
+async function checkDependencies() {
+  try {
+    logger.info('正在检查系统依赖...');
+    
+    // 检查 Node.js 版本
+    const nodeVersion = process.version;
+    const minNodeVersion = 'v14.0.0';
+    if (compareVersions(nodeVersion, minNodeVersion) < 0) {
+      logger.warn(`当前 Node.js 版本 ${nodeVersion} 低于推荐的最低版本 ${minNodeVersion}`);
+    } else {
+      logger.success(`Node.js 版本 ${nodeVersion} 满足要求`);
+    }
+    
+    // 检查必要的 npm 包
+    try {
+      const packageJson = require('../package.json');
+      const requiredDeps = Object.keys(packageJson.dependencies);
+      
+      logger.info(`系统依赖共 ${requiredDeps.length} 个包`);
+      
+      // 检查是否有 node_modules 目录
+      try {
+        await fs.access(path.join(__dirname, '..', 'node_modules'));
+      } catch (err) {
+        if (err.code === 'ENOENT') {
+          logger.warn('未找到 node_modules 目录,请运行 npm install 安装依赖');
+          return false;
+        }
+      }
+    } catch (err) {
+      logger.warn('无法读取 package.json:', err.message);
+    }
+    
+    // 检查 Docker
+    try {
+      execSync('docker --version', { stdio: ['ignore', 'ignore', 'ignore'] });
+      logger.success('Docker 已安装');
+    } catch (err) {
+      logger.warn('未检测到 Docker,部分功能可能无法正常使用');
+    }
+    
+    return true;
+  } catch (error) {
+    logger.error('依赖检查失败:', error);
+    return false;
+  }
+}
+
+/**
+ * 比较版本号
+ */
+function compareVersions(v1, v2) {
+  const v1parts = v1.replace('v', '').split('.');
+  const v2parts = v2.replace('v', '').split('.');
+  
+  for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
+    const v1part = parseInt(v1parts[i] || 0);
+    const v2part = parseInt(v2parts[i] || 0);
+    
+    if (v1part > v2part) return 1;
+    if (v1part < v2part) return -1;
+  }
+  
+  return 0;
+}
+
+/**
+ * 主初始化函数
+ */
+async function initialize() {
+  logger.info('开始系统初始化...');
+  
+  try {
+    // 1. 检查系统依赖
+    await checkDependencies();
+    
+    // 2. 确保目录结构存在
+    await ensureDirectoriesExist();
+    logger.success('目录结构初始化完成');
+    
+    // 3. 下载必要图片
+    await downloadImages();
+    
+    // 4. 创建默认用户
+    await createAdminUser();
+    
+    // 5. 创建默认配置
+    await createDefaultConfig();
+    
+    // 6. 创建示例文档
+    await createSampleDocumentation();
+    
+    logger.success('系统初始化完成!');
+    // 移除敏感的账户信息日志
+    logger.warn('首次登录后请立即修改默认密码!');
+    
+    return { success: true };
+  } catch (error) {
+    logger.error('系统初始化失败:', error);
+    return { success: false, error: error.message };
+  }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+  initialize()
+    .then((result) => {
+      if (result.success) {
+        process.exit(0);
+      } else {
+        process.exit(1);
+      }
+    })
+    .catch((error) => {
+      logger.fatal('初始化过程中发生错误:', error);
+      process.exit(1);
+    });
+}
+
+module.exports = {
+  initialize,
+  createAdminUser,
+  createDefaultConfig,
+  createSampleDocumentation,
+  checkDependencies
+};

+ 333 - 0
hubcmdui/server-utils.js

@@ -0,0 +1,333 @@
+/**
+ * 服务器实用工具函数
+ */
+
+const { exec } = require('child_process');
+const os = require('os');
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('./logger');
+
+/**
+ * 安全执行系统命令
+ * @param {string} command - 要执行的命令
+ * @param {object} options - 执行选项
+ * @returns {Promise<string>} 命令输出结果
+ */
+function execCommand(command, options = { timeout: 30000 }) {
+  return new Promise((resolve, reject) => {
+    exec(command, options, (error, stdout, stderr) => {
+      if (error) {
+        if (error.killed) {
+          reject(new Error('执行命令超时'));
+        } else {
+          reject(error);
+        }
+        return;
+      }
+      resolve(stdout.trim() || stderr.trim());
+    });
+  });
+}
+
+/**
+ * 获取系统信息
+ * @returns {Promise<object>} 系统信息对象
+ */
+async function getSystemInfo() {
+  const platform = os.platform();
+  let memoryInfo = {};
+  let cpuInfo = {};
+  let diskInfo = {};
+  
+  try {
+    // 内存信息 - 使用OS模块,适用于所有平台
+    const totalMem = os.totalmem();
+    const freeMem = os.freemem();
+    const usedMem = totalMem - freeMem;
+    const memPercent = Math.round((usedMem / totalMem) * 100);
+    
+    memoryInfo = {
+      total: formatBytes(totalMem),
+      free: formatBytes(freeMem),
+      used: formatBytes(usedMem),
+      percent: memPercent
+    };
+    
+    // CPU信息 - 使用不同平台的方法
+    await getCpuInfo(platform).then(info => {
+      cpuInfo = info;
+    }).catch(err => {
+      logger.warn('获取CPU信息失败:', err);
+      // 降级方案:使用OS模块
+      const cpuLoad = os.loadavg();
+      const cpuCores = os.cpus().length;
+      
+      cpuInfo = {
+        model: os.cpus()[0].model,
+        cores: cpuCores,
+        load1: cpuLoad[0].toFixed(2),
+        load5: cpuLoad[1].toFixed(2),
+        load15: cpuLoad[2].toFixed(2),
+        percent: Math.round((cpuLoad[0] / cpuCores) * 100)
+      };
+    });
+    
+    // 磁盘信息 - 根据平台调用不同方法
+    await getDiskInfo(platform).then(info => {
+      diskInfo = info;
+    }).catch(err => {
+      logger.warn('获取磁盘信息失败:', err);
+      diskInfo = {
+        filesystem: 'unknown',
+        size: 'unknown',
+        used: 'unknown',
+        available: 'unknown',
+        percent: '0%'
+      };
+    });
+    
+    return {
+      platform,
+      hostname: os.hostname(),
+      memory: memoryInfo,
+      cpu: cpuInfo,
+      disk: diskInfo,
+      uptime: formatUptime(os.uptime())
+    };
+  } catch (error) {
+    logger.error('获取系统信息失败:', error);
+    throw error;
+  }
+}
+
+/**
+ * 根据平台获取CPU信息
+ * @param {string} platform - 操作系统平台
+ * @returns {Promise<object>} CPU信息
+ */
+async function getCpuInfo(platform) {
+  if (platform === 'linux') {
+    try {
+      // Linux平台使用/proc/stat和/proc/cpuinfo
+      const [loadData, cpuData] = await Promise.all([
+        execCommand("cat /proc/loadavg"),
+        execCommand("cat /proc/cpuinfo | grep 'model name' | head -1")
+      ]);
+      
+      const cpuLoad = loadData.split(' ').slice(0, 3).map(parseFloat);
+      const cpuCores = os.cpus().length;
+      const modelMatch = cpuData.match(/model name\s*:\s*(.*)/);
+      const model = modelMatch ? modelMatch[1].trim() : os.cpus()[0].model;
+      const percent = Math.round((cpuLoad[0] / cpuCores) * 100);
+      
+      return {
+        model,
+        cores: cpuCores,
+        load1: cpuLoad[0].toFixed(2),
+        load5: cpuLoad[1].toFixed(2), 
+        load15: cpuLoad[2].toFixed(2),
+        percent: percent > 100 ? 100 : percent
+      };
+    } catch (error) {
+      throw error;
+    }
+  } else if (platform === 'darwin') {
+    // macOS平台
+    try {
+      const cpuLoad = os.loadavg();
+      const cpuCores = os.cpus().length;
+      const model = os.cpus()[0].model;
+      const systemProfilerData = await execCommand("system_profiler SPHardwareDataType | grep 'Processor Name'");
+      const cpuMatch = systemProfilerData.match(/Processor Name:\s*(.*)/);
+      const cpuModel = cpuMatch ? cpuMatch[1].trim() : model;
+      
+      return {
+        model: cpuModel,
+        cores: cpuCores,
+        load1: cpuLoad[0].toFixed(2),
+        load5: cpuLoad[1].toFixed(2),
+        load15: cpuLoad[2].toFixed(2),
+        percent: Math.round((cpuLoad[0] / cpuCores) * 100)
+      };
+    } catch (error) {
+      throw error;
+    }
+  } else if (platform === 'win32') {
+    // Windows平台
+    try {
+      // 使用wmic获取CPU信息
+      const cpuData = await execCommand('wmic cpu get Name,NumberOfCores /value');
+      const cpuLines = cpuData.split('\r\n');
+      
+      let model = os.cpus()[0].model;
+      let cores = os.cpus().length;
+      
+      cpuLines.forEach(line => {
+        if (line.startsWith('Name=')) {
+          model = line.substring(5).trim();
+        } else if (line.startsWith('NumberOfCores=')) {
+          cores = parseInt(line.substring(14).trim()) || cores;
+        }
+      });
+      
+      // Windows没有直接的负载平均值,使用CPU使用率作为替代
+      const perfData = await execCommand('wmic cpu get LoadPercentage /value');
+      const loadMatch = perfData.match(/LoadPercentage=(\d+)/);
+      const loadPercent = loadMatch ? parseInt(loadMatch[1]) : 0;
+      
+      return {
+        model,
+        cores,
+        load1: '不适用',
+        load5: '不适用',
+        load15: '不适用',
+        percent: loadPercent
+      };
+    } catch (error) {
+      throw error;
+    }
+  }
+  
+  // 默认返回OS模块的信息
+  const cpuLoad = os.loadavg();
+  const cpuCores = os.cpus().length;
+  
+  return {
+    model: os.cpus()[0].model,
+    cores: cpuCores,
+    load1: cpuLoad[0].toFixed(2),
+    load5: cpuLoad[1].toFixed(2),
+    load15: cpuLoad[2].toFixed(2),
+    percent: Math.round((cpuLoad[0] / cpuCores) * 100)
+  };
+}
+
+/**
+ * 根据平台获取磁盘信息
+ * @param {string} platform - 操作系统平台
+ * @returns {Promise<object>} 磁盘信息
+ */
+async function getDiskInfo(platform) {
+  if (platform === 'linux' || platform === 'darwin') {
+    try {
+      // Linux/macOS使用df命令
+      const diskCommand = platform === 'linux' 
+        ? 'df -h / | tail -1' 
+        : 'df -h / | tail -1';
+      
+      const diskData = await execCommand(diskCommand);
+      const parts = diskData.trim().split(/\s+/);
+      
+      if (parts.length >= 5) {
+        return {
+          filesystem: parts[0],
+          size: parts[1],
+          used: parts[2],
+          available: parts[3],
+          percent: parts[4]
+        };
+      } else {
+        throw new Error('磁盘信息格式不正确');
+      }
+    } catch (error) {
+      throw error;
+    }
+  } else if (platform === 'win32') {
+    // Windows平台
+    try {
+      // 使用wmic获取C盘信息
+      const diskData = await execCommand('wmic logicaldisk where DeviceID="C:" get Size,FreeSpace /value');
+      const lines = diskData.split(/\r\n|\n/);
+      let freeSpace, totalSize;
+      
+      lines.forEach(line => {
+        if (line.startsWith('FreeSpace=')) {
+          freeSpace = parseInt(line.split('=')[1]);
+        } else if (line.startsWith('Size=')) {
+          totalSize = parseInt(line.split('=')[1]);
+        }
+      });
+      
+      if (freeSpace !== undefined && totalSize !== undefined) {
+        const usedSpace = totalSize - freeSpace;
+        const usedPercent = Math.round((usedSpace / totalSize) * 100);
+        
+        return {
+          filesystem: 'C:',
+          size: formatBytes(totalSize),
+          used: formatBytes(usedSpace),
+          available: formatBytes(freeSpace),
+          percent: `${usedPercent}%`
+        };
+      } else {
+        throw new Error('无法解析Windows磁盘信息');
+      }
+    } catch (error) {
+      throw error;
+    }
+  }
+  
+  // 默认尝试df命令
+  try {
+    const diskData = await execCommand('df -h / | tail -1');
+    const parts = diskData.trim().split(/\s+/);
+    
+    if (parts.length >= 5) {
+      return {
+        filesystem: parts[0],
+        size: parts[1],
+        used: parts[2],
+        available: parts[3],
+        percent: parts[4]
+      };
+    } else {
+      throw new Error('磁盘信息格式不正确');
+    }
+  } catch (error) {
+    throw error;
+  }
+}
+
+/**
+ * 将字节格式化为可读大小
+ * @param {number} bytes - 字节数
+ * @returns {string} 格式化后的字符串
+ */
+function formatBytes(bytes) {
+  if (bytes === 0) return '0 B';
+  
+  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+  const i = Math.floor(Math.log(bytes) / Math.log(1024));
+  
+  return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
+}
+
+/**
+ * 格式化运行时间
+ * @param {number} seconds - 秒数
+ * @returns {string} 格式化后的运行时间
+ */
+function formatUptime(seconds) {
+  const days = Math.floor(seconds / 86400);
+  seconds %= 86400;
+  const hours = Math.floor(seconds / 3600);
+  seconds %= 3600;
+  const minutes = Math.floor(seconds / 60);
+  seconds = Math.floor(seconds % 60);
+  
+  const parts = [];
+  if (days > 0) parts.push(`${days}天`);
+  if (hours > 0) parts.push(`${hours}小时`);
+  if (minutes > 0) parts.push(`${minutes}分钟`);
+  if (seconds > 0 && parts.length === 0) parts.push(`${seconds}秒`);
+  
+  return parts.join(' ');
+}
+
+module.exports = {
+  execCommand,
+  getSystemInfo,
+  formatBytes,
+  formatUptime
+};

+ 149 - 1724
hubcmdui/server.js

@@ -1,1796 +1,221 @@
+/**
+ * Docker 镜像代理加速系统 - 服务器入口点
+ */
 const express = require('express');
 const fs = require('fs').promises;
 const path = require('path');
 const bodyParser = require('body-parser');
 const session = require('express-session');
-const bcrypt = require('bcrypt');
-const crypto = require('crypto');
-const axios = require('axios'); // 用于发送 HTTP 请求
-const Docker = require('dockerode');
-const app = express();
 const cors = require('cors');
-const WebSocket = require('ws');
 const http = require('http');
-const { exec } = require('child_process'); // 网络测试
-const validator = require('validator');
 const logger = require('./logger');
-const pLimit = require('p-limit');
-const axiosRetry = require('axios-retry');
-const NodeCache = require('node-cache');
+const { ensureDirectoriesExist } = require('./init-dirs');
+const { downloadImages } = require('./download-images');
+const { gracefulShutdown } = require('./cleanup');
+const os = require('os');
+const { requireLogin } = require('./middleware/auth');
+const compatibilityLayer = require('./compatibility-layer');
+const initSystem = require('./scripts/init-system');
 
-// 创建请求缓存,TTL为10分钟
-const requestCache = new NodeCache({ stdTTL: 600, checkperiod: 120 });
+// 设置日志级别 (默认INFO, 可通过环境变量设置)
+const logLevel = process.env.LOG_LEVEL || 'INFO';
+logger.setLogLevel(logLevel);
+logger.info(`日志级别已设置为: ${logLevel}`);
 
-// 配置并发限制,最多5个并发请求
-const limit = pLimit(5);
+// 导入配置
+const config = require('./config');
 
-// 配置Axios重试
-axiosRetry(axios, {
-  retries: 3, // 最多重试3次
-  retryDelay: (retryCount) => {
-    console.log(`[INFO] 重试 Docker Hub 请求 (${retryCount}/3)`);
-    return retryCount * 1000; // 重试延迟,每次递增1秒
-  },
-  retryCondition: (error) => {
-    // 只在网络错误或5xx响应时重试
-    return axiosRetry.isNetworkOrIdempotentRequestError(error) || 
-           (error.response && error.response.status >= 500);
-  }
-});
+// 导入中间件
+const { sessionActivity, sanitizeRequestBody, securityHeaders } = require('./middleware/auth');
 
-// 优化HTTP请求配置
-const httpOptions = {
-  timeout: 15000, // 15秒超时
-  headers: {
-    'User-Agent': 'DockerHubSearchClient/1.0',
-    'Accept': 'application/json'
-  }
-};
+// 导入初始化调度器
+const { executeOnce } = require('./lib/initScheduler');
 
-let docker = null;
-let containerStates = new Map();
-let lastStopAlertTime = new Map();
-let secondAlertSent = new Set();
-
-async function initDocker() {
-  if (docker === null) {
-    docker = new Docker();
-    try {
-      await docker.ping();
-      logger.success('成功连接到 Docker 守护进程');
-    } catch (err) {
-      logger.error(`无法连接到 Docker 守护进程: ${err.message}`);
-      docker = null;
-    }
-  }
-  return docker;
-}
+// 初始化Express应用
+const app = express();
+const server = http.createServer(app);
 
+// 配置中间件
 app.use(cors());
 app.use(express.json());
 app.use(express.static('web'));
 app.use(bodyParser.urlencoded({ extended: true }));
 app.use(session({
-  secret: 'OhTq3faqSKoxbV%NJV',
-  resave: false,
+  secret: config.sessionSecret || 'OhTq3faqSKoxbV%NJV',
+  resave: true,
   saveUninitialized: true,
-  cookie: { secure: false } // 设置为true如果使用HTTPS
+  cookie: { 
+    secure: config.secureSession || false,
+    maxAge: 7 * 24 * 60 * 60 * 1000 // 7天(一周)
+  }
 }));
 
-// 添加请求日志中间件
+// 自定义中间件
+app.use(sessionActivity);
+app.use(sanitizeRequestBody);
+app.use(securityHeaders);
+
+// 请求日志中间件
 app.use((req, res, next) => {
   const start = Date.now();
   
   // 在响应完成后记录日志
   res.on('finish', () => {
     const duration = Date.now() - start;
-    logger.request(req, res, duration);
-  });
-  
-  next();
-});
-
-// 替换之前的morgan中间件
-// app.use(require('morgan')('dev'));
-
-// 正确顺序注册API路由,避免冲突
-// 先注册特定路由,后注册通用路由
-
-app.get('/admin', (req, res) => {
-  res.sendFile(path.join(__dirname, 'web', 'admin.html'));
-});
-
-// 新增:Docker Hub 搜索 API
-app.get('/api/search', async (req, res) => {
-  const searchTerm = req.query.term;
-  if (!searchTerm) {
-    return res.status(400).json({ error: 'Search term is required' });
-  }
-
-  try {
-    const response = await axios.get(`https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(searchTerm)}`);
-    res.json(response.data);
-  } catch (error) {
-    logger.error('Error searching Docker Hub:', error);
-    res.status(500).json({ error: 'Failed to search Docker Hub' });
-  }
-});
-
-// 代理Docker Hub搜索API
-app.get('/api/dockerhub/search', async (req, res) => {
-  const term = req.query.term;
-  const page = req.query.page || 1;
-  
-  if (!term) {
-    return res.status(400).json({ error: '搜索词不能为空' });
-  }
-  
-  try {
-    const cacheKey = `search_${term}_${page}`;
-    const cachedResult = requestCache.get(cacheKey);
-    
-    if (cachedResult) {
-      console.log(`[INFO] 返回缓存的搜索结果: ${term} (页码: ${page})`);
-      return res.json(cachedResult);
-    }
-    
-    console.log(`[INFO] 搜索Docker Hub: ${term} (页码: ${page})`);
-    
-    // 使用pLimit进行并发控制的请求
-    const result = await limit(async () => {
-      const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`;
-      const response = await axios.get(url, httpOptions);
-      return response.data;
-    });
-    
-    // 将结果缓存
-    requestCache.set(cacheKey, result);
-    res.json(result);
-    
-  } catch (error) {
-    handleAxiosError(error, res, '搜索Docker Hub失败');
-  }
-});
-
-// 代理Docker Hub TAG API - 改进异常处理和错误响应格式以及过滤无效平台信息
-app.get('/api/dockerhub/tags', async (req, res) => {
-  const name = req.query.name;
-  const isOfficial = req.query.official === 'true';
-  const page = req.query.page || 1;
-  const pageSize = req.query.page_size || 25;
-  
-  if (!name) {
-    return res.status(400).json({ error: '镜像名称不能为空' });
-  }
-  
-  try {
-    const cacheKey = `tags_${name}_${isOfficial}_${page}_${pageSize}`;
-    const cachedResult = requestCache.get(cacheKey);
-    
-    if (cachedResult) {
-      console.log(`[INFO] 返回缓存的标签列表: ${name} (页码: ${page}, 每页数量: ${pageSize})`);
-      return res.json(cachedResult);
-    }
-    
-    let apiUrl;
-    if (isOfficial) {
-      apiUrl = `https://hub.docker.com/v2/repositories/library/${name}/tags/?page=${page}&page_size=${pageSize}`;
-    } else {
-      apiUrl = `https://hub.docker.com/v2/repositories/${name}/tags/?page=${page}&page_size=${pageSize}`;
-    }
-    
-    // 使用pLimit进行并发控制的请求
-    const result = await limit(async () => {
-      const response = await axios.get(apiUrl, httpOptions);
-      return response.data;
-    });
-    
-    // 对结果进行预处理,确保images字段存在
-    if (result.results) {
-      result.results.forEach(tag => {
-        if (!tag.images || !Array.isArray(tag.images)) {
-          tag.images = [];
-        }
-      });
-    }
-    
-    // 将结果缓存
-    requestCache.set(cacheKey, result);
-    res.json(result);
-    
-  } catch (error) {
-    handleAxiosError(error, res, '获取标签列表失败');
-  }
-});
-
-// 代理Docker Hub TAG API - 改进异常处理和错误响应格式以及过滤无效平台信息
-app.get('/api/dockerhub/tags', async (req, res) => {
-  try {
-    const imageName = req.query.name;
-    const isOfficial = req.query.official === 'true';
-    const page = parseInt(req.query.page) || 1;
-    const page_size = parseInt(req.query.page_size) || 25; // 默认改为25个标签
-    const getAllTags = req.query.all === 'true'; // 是否获取所有标签
     
-    if (!imageName) {
-      return res.status(400).json({ error: '镜像名称不能为空' });
-    }
-
-    // 构建基本参数
-    const fullImageName = isOfficial ? `library/${imageName}` : imageName;
-    
-    if (getAllTags) {
-      try {
-        logger.info(`获取所有镜像标签: ${fullImageName}`);
-        // 为所有标签请求设置超时限制
-        const allTagsPromise = fetchAllTags(fullImageName);
-        const timeoutPromise = new Promise((_, reject) => 
-          setTimeout(() => reject(new Error('获取所有标签超时')), 30000)
-        );
-        
-        // 使用Promise.race确保请求不会无限等待
-        const allTags = await Promise.race([allTagsPromise, timeoutPromise]);
-        
-        // 过滤掉无效平台信息
-        const cleanedTags = allTags.map(tag => {
-          if (tag.images && Array.isArray(tag.images)) {
-            tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
-          }
-          return tag;
-        });
-        
-        return res.json({
-          count: cleanedTags.length,
-          results: cleanedTags,
-          all_pages_loaded: true
-        });
-      } catch (error) {
-        logger.error(`获取所有标签失败: ${error.message}`);
-        return res.status(500).json({ error: `获取所有标签失败: ${error.message}` });
-      }
-    } else {
-      // 常规分页获取
-      logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 页面大小: ${page_size}`);
-      const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${page_size}`;
+    // 增强过滤条件
+    const isSuccessfulGet = req.method === 'GET' && (res.statusCode === 200 || res.statusCode === 304);
+    const isStaticResource = req.url.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i);
+    const isCommonApiRequest = req.url.startsWith('/api/') && 
+                              (req.url.includes('/check-session') || 
+                               req.url.includes('/system-resources') ||
+                               req.url.includes('/docker/status'));
+    const isErrorResponse = res.statusCode >= 400;
+    
+    // 只记录关键API请求和错误响应,过滤普通的API请求和静态资源
+    if ((isErrorResponse || 
+        (req.url.startsWith('/api/') && !isCommonApiRequest)) && 
+        !isStaticResource && 
+        !(isSuccessfulGet && isCommonApiRequest)) {
       
-      try {
-        // 添加超时和重试配置
-        const tagsResponse = await axios.get(tagsUrl, {
-          timeout: 15000, // 15秒超时
-          headers: {
-            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
-          }
-        });
-        
-        // 检查是否有有效的响应数据
-        if (!tagsResponse.data || typeof tagsResponse.data !== 'object') {
-          logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`);
-          return res.status(500).json({ error: `获取标签列表失败: 响应数据格式不正确` });
-        }
-        
-        if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) {
-          logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`);
-          return res.json({ count: 0, results: [] });
-        }
-        
-        // 过滤掉无效平台信息
-        const cleanedResults = tagsResponse.data.results.map(tag => {
-          if (tag.images && Array.isArray(tag.images)) {
-            tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
-          }
-          return tag;
-        });
-        
-        return res.json({
-          ...tagsResponse.data,
-          results: cleanedResults
-        });
-      } catch (error) {
-        // 更详细的错误日志记录和响应
-        logger.error(`获取标签列表失败: ${error.message}`, {
-          url: tagsUrl,
-          status: error.response?.status,
-          statusText: error.response?.statusText,
-          data: error.response?.data
-        });
-        
-        // 确保返回一个格式化良好的错误响应
-        return res.status(500).json({ 
-          error: `获取标签列表失败: ${error.message}`, 
-          details: error.response?.data || error.message 
-        });
-      }
-    }
-  } catch (error) {
-    logger.error(`获取TAG失败:`, error.message);
-    return res.status(500).json({ error: '获取TAG失败,请稍后重试', details: error.message });
-  }
-});
-
-// 辅助函数: 递归获取所有标签 - 修复错误处理和添加页面限制
-async function fetchAllTags(fullImageName, page = 1, allTags = [], maxPages = 10) {
-  try {
-    // 限制最大页数,防止无限递归
-    if (page > maxPages) {
-      logger.warn(`达到最大页数限制 (${maxPages}),停止获取更多标签`);
-      return allTags;
-    }
-    
-    const pageSize = 100; // 使用最大页面大小
-    const url = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`;
-    
-    logger.info(`获取标签页 ${page}/${maxPages}...`);
-    
-    const response = await axios.get(url, {
-      timeout: 10000, // 10秒超时
-      headers: {
-        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
-      }
-    });
-    
-    if (!response.data.results || !Array.isArray(response.data.results)) {
-      logger.warn(`页 ${page} 没有有效的标签数据`);
-      return allTags;
-    }
-    
-    allTags.push(...response.data.results);
-    logger.info(`已获取 ${allTags.length}/${response.data.count || 'unknown'} 个标签`);
-    
-    // 检查是否有下一页
-    if (response.data.next && allTags.length < response.data.count) {
-      // 添加一些延迟以避免请求过快
-      await new Promise(resolve => setTimeout(resolve, 500));
-      return fetchAllTags(fullImageName, page + 1, allTags, maxPages);
-    }
-    
-    logger.success(`成功获取所有 ${allTags.length} 个标签`);
-    return allTags;
-  } catch (error) {
-    logger.error(`递归获取标签失败 (页码 ${page}): ${error.message}`);
-    // 如果已经获取了一些标签,返回这些标签而不是抛出错误
-    if (allTags.length > 0) {
-      logger.info(`尽管出错,仍返回已获取的 ${allTags.length} 个标签`);
-      return allTags;
-    }
-    throw error; // 如果没有获取到任何标签,则抛出错误
-  }
-}
-
-// API 端点: 获取镜像标签计数 - 修复路由定义
-app.get('/api/dockerhub/tag-count', async (req, res) => {
-  const name = req.query.name;
-  const isOfficial = req.query.official === 'true';
-  
-  if (!name) {
-    return res.status(400).json({ error: '镜像名称不能为空' });
-  }
-  
-  try {
-    const cacheKey = `tag_count_${name}_${isOfficial}`;
-    const cachedResult = requestCache.get(cacheKey);
-    
-    if (cachedResult) {
-      console.log(`[INFO] 返回缓存的标签计数: ${name}`);
-      return res.json(cachedResult);
-    }
-    
-    let apiUrl;
-    if (isOfficial) {
-      apiUrl = `https://hub.docker.com/v2/repositories/library/${name}/tags/?page_size=1`;
-    } else {
-      apiUrl = `https://hub.docker.com/v2/repositories/${name}/tags/?page_size=1`;
-    }
-    
-    // 使用pLimit进行并发控制的请求
-    const result = await limit(async () => {
-      const response = await axios.get(apiUrl, httpOptions);
-      return {
-        count: response.data.count,
-        recommended_mode: response.data.count > 500 ? 'paginated' : 'full'
-      };
-    });
-    
-    // 将结果缓存
-    requestCache.set(cacheKey, result);
-    res.json(result);
-    
-  } catch (error) {
-    handleAxiosError(error, res, '获取标签计数失败');
-  }
-});
-
-const CONFIG_FILE = path.join(__dirname, 'config.json');
-const USERS_FILE = path.join(__dirname, 'users.json');
-const DOCUMENTATION_DIR = path.join(__dirname, 'documentation');
-const DOCUMENTATION_FILE = path.join(__dirname, 'documentation.md');
-
-// 读取配置
-async function readConfig() {
-  try {
-    const data = await fs.readFile(CONFIG_FILE, 'utf8');
-    let config;
-    if (!data.trim()) {
-      config = {
-        logo: '',
-        menuItems: [],
-        adImages: [],
-        monitoringConfig: {
-          webhookUrl: '',
-          monitorInterval: 60,
-          isEnabled: false
-        }
-      };
-    } else {
-      config = JSON.parse(data);
-    }
-    
-    // 确保 monitoringConfig 存在,如果不存在则添加默认值
-    if (!config.monitoringConfig) {
-      config.monitoringConfig = {
-        webhookUrl: '',
-        monitorInterval: 60,
-        isEnabled: false
-      };
-    }
-    
-    return config;
-  } catch (error) {
-    logger.error('Failed to read config:', error);
-    if (error.code === 'ENOENT') {
-      return {
-        logo: '',
-        menuItems: [],
-        adImages: [],
-        monitoringConfig: {
-          webhookUrl: '',
-          monitorInterval: 60,
-          isEnabled: false
-        }
-      };
-    }
-    throw error;
-  }
-}
-
-// 写入配置
-async function writeConfig(config) {
-  try {
-    await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
-    logger.success('Config saved successfully');
-  } catch (error) {
-    logger.error('Failed to save config:', error);
-    throw error;
-  }
-}
-
-// 读取用户
-async function readUsers() {
-  try {
-    const data = await fs.readFile(USERS_FILE, 'utf8');
-    return JSON.parse(data);
-  } catch (error) {
-    if (error.code === 'ENOENT') {
-      logger.warn('Users file does not exist, creating default user');
-      const defaultUser = { username: 'root', password: bcrypt.hashSync('admin', 10) };
-      await writeUsers([defaultUser]);
-      return { users: [defaultUser] };
-    }
-    throw error;
-  }
-}
-
-// 写入用户
-async function writeUsers(users) {
-  await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8');
-}
-
-// 确保 documentation 目录存在
-async function ensureDocumentationDir() {
-  try {
-    await fs.access(DOCUMENTATION_DIR);
-  } catch (error) {
-    if (error.code === 'ENOENT') {
-      await fs.mkdir(DOCUMENTATION_DIR);
-    } else {
-      throw error;
-    }
-  }
-}
-
-// 读取文档
-async function readDocumentation() {
-  try {
-    await ensureDocumentationDir();
-    const files = await fs.readdir(DOCUMENTATION_DIR);
-    const documents = await Promise.all(files.map(async file => {
-      const filePath = path.join(DOCUMENTATION_DIR, file);
-      const content = await fs.readFile(filePath, 'utf8');
-      const doc = JSON.parse(content);
-      return {
-        id: path.parse(file).name,
-        title: doc.title,
-        content: doc.content,
-        published: doc.published
-      };
-    }));
-
-    const publishedDocuments = documents.filter(doc => doc.published);
-    return publishedDocuments;
-  } catch (error) {
-    logger.error('Error reading documentation:', error);
-    throw error;
-  }
-}
-
-// 写入文档
-async function writeDocumentation(content) {
-  await fs.writeFile(DOCUMENTATION_FILE, content, 'utf8');
-}
-
-// 登录验证
-app.post('/api/login', async (req, res) => {
-  const { username, password, captcha } = req.body;
-  if (req.session.captcha !== parseInt(captcha)) {
-    logger.warn(`Captcha verification failed for user: ${username}`);
-    return res.status(401).json({ error: '验证码错误' });
-  }
-
-  const users = await readUsers();
-  const user = users.users.find(u => u.username === username);
-  if (!user) {
-    logger.warn(`User ${username} not found`);
-    return res.status(401).json({ error: '用户名或密码错误' });
-  }
-
-  if (bcrypt.compareSync(req.body.password, user.password)) {
-    req.session.user = { username: user.username };
-    logger.info(`User ${username} logged in successfully`);
-    res.json({ success: true });
-  } else {
-    logger.warn(`Login failed for user: ${username}`);
-    res.status(401).json({ error: '用户名或密码错误' });
-  }
-});
-
-// 修改密码
-app.post('/api/change-password', async (req, res) => {
-  if (!req.session.user) {
-    return res.status(401).json({ error: 'Not logged in' });
-  }
-  const { currentPassword, newPassword } = req.body;
-  const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
-  if (!passwordRegex.test(newPassword)) {
-    return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter, one number, and one special character' });
-  }
-  const users = await readUsers();
-  const user = users.users.find(u => u.username === req.session.user.username);
-  if (user && bcrypt.compareSync(currentPassword, user.password)) {
-    user.password = bcrypt.hashSync(newPassword, 10);
-    await writeUsers(users.users);
-    res.json({ success: true });
-  } else {
-    res.status(401).json({ error: 'Invalid current password' });
-  }
-});
-
-// 需要登录验证的中间件
-function requireLogin(req, res, next) {
-  // 不再记录会话详细信息
-  if (req.session.user) {
-    next();
-  } else {
-    logger.warn('用户未登录');
-    res.status(401).json({ error: 'Not logged in' });
-  }
-}
-
-// API 端点:获取配置
-app.get('/api/config', async (req, res) => {
-  try {
-    const config = await readConfig();
-    res.json(config);
-  } catch (error) {
-    res.status(500).json({ error: 'Failed to read config' });
-  }
-});
-
-// API 端点:保存配置
-app.post('/api/config', requireLogin, async (req, res) => {
-  try {
-    const currentConfig = await readConfig();
-    const newConfig = { ...currentConfig, ...req.body };
-    await writeConfig(newConfig);
-    res.json({ success: true });
-  } catch (error) {
-    res.status(500).json({ error: 'Failed to save config' });
-  }
-});
-
-// API 端点:检查会话状态
-app.get('/api/check-session', (req, res) => {
-  if (req.session.user) {
-    res.json({ success: true });
-  } else {
-    res.status(401).json({ error: 'Not logged in' });
-  }
-});
-
-// API 端点:生成验证码
-app.get('/api/captcha', (req, res) => {
-  const num1 = Math.floor(Math.random() * 10);
-  const num2 = Math.floor(Math.random() * 10);
-  const captcha = `${num1} + ${num2} = ?`;
-  req.session.captcha = num1 + num2;
-  res.json({ captcha });
-});
-
-// API端点:获取文档列表
-app.get('/api/documentation-list', requireLogin, async (req, res) => {
-  try {
-    const files = await fs.readdir(DOCUMENTATION_DIR);
-    const documents = await Promise.all(files.map(async file => {
-      const content = await fs.readFile(path.join(DOCUMENTATION_DIR, file), 'utf8');
-      const doc = JSON.parse(content);
-      return { id: path.parse(file).name, ...doc };
-    }));
-    res.json(documents);
-  } catch (error) {
-    res.status(500).json({ error: '读取文档列表失败' });
-  }
-});
-
-// API端点:保存文档
-app.post('/api/documentation', requireLogin, async (req, res) => {
-  try {
-    const { id, title, content } = req.body;
-    const docId = id || Date.now().toString();
-    const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
-    await fs.writeFile(docPath, JSON.stringify({ title, content, published: false }));
-    res.json({ success: true });
-  } catch (error) {
-    res.status(500).json({ error: '保存文档失败' });
-  }
-});
-
-// API端点:删除文档
-app.delete('/api/documentation/:id', requireLogin, async (req, res) => {
-  try {
-    const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
-    await fs.unlink(docPath);
-    res.json({ success: true });
-  } catch (error) {
-    res.status(500).json({ error: '删除文档失败' });
-  }
-});
-
-// API端点:切换文档发布状态
-app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res) => {
-  try {
-    const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
-    const content = await fs.readFile(docPath, 'utf8');
-    const doc = JSON.parse(content);
-    doc.published = !doc.published;
-    await fs.writeFile(docPath, JSON.stringify(doc));
-    res.json({ success: true });
-  } catch (error) {
-    res.status(500).json({ error: '更改发布状态失败' });
-  }
-});
-
-// API端点:获取文档
-app.get('/api/documentation', async (req, res) => {
-  try {
-    const documents = await readDocumentation();
-    res.json(documents);
-  } catch (error) {
-    logger.error('Error in /api/documentation:', error);
-    res.status(500).json({ error: '读取文档失败', details: error.message });
-  }
-});
-
-// API端点:保存文档
-app.post('/api/documentation', requireLogin, async (req, res) => {
-  try {
-    const { content } = req.body;
-    await writeDocumentation(content);
-    res.json({ success: true });
-  } catch (error) {
-    res.status(500).json({ error: '保存文档失败' });
-  }
-});
-
-// 获取文档列表函数
-async function getDocumentList() {
-  try {
-    await ensureDocumentationDir();
-    const files = await fs.readdir(DOCUMENTATION_DIR);
-    logger.info('Files in documentation directory:', files);
-
-    const documents = await Promise.all(files.map(async file => {
-      try {
-        const filePath = path.join(DOCUMENTATION_DIR, file);
-        const content = await fs.readFile(filePath, 'utf8');
-        return {
-          id: path.parse(file).name,
-          title: path.parse(file).name, // 使用文件名作为标题
-          content: content,
-          published: true // 假设所有文档都是已发布的
-        };
-      } catch (fileError) {
-        logger.error(`Error reading file ${file}:`, fileError);
-        return null;
-      }
-    }));
-
-    const validDocuments = documents.filter(doc => doc !== null);
-    logger.info('Valid documents:', validDocuments);
-
-    return validDocuments;
-  } catch (error) {
-    logger.error('Error reading document list:', error);
-    throw error; // 重新抛出错误,让上层函数处理
-  }
-}
-
-app.get('/api/documentation-list', async (req, res) => {
-  try {
-    const documents = await getDocumentList();
-    res.json(documents);
-  } catch (error) {
-    logger.error('Error in /api/documentation-list:', error);
-    res.status(500).json({ 
-      error: '读取文档列表失败', 
-      details: error.message, 
-      stack: error.stack 
-    });
-  }
-});
-
-app.get('/api/documentation/:id', async (req, res) => {
-  try {
-    const docId = req.params.id;
-    const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
-    const content = await fs.readFile(docPath, 'utf8');
-    const doc = JSON.parse(content);
-    res.json(doc);
-  } catch (error) {
-    logger.error('Error reading document:', error);
-    res.status(500).json({ error: '读取文档失败', details: error.message });
-  }
-});
-
-// API端点来获取Docker容器状态
-app.get('/api/docker-status', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const containers = await docker.listContainers({ all: true });
-    const containerStatus = await Promise.all(containers.map(async (container) => {
-      const containerInfo = await docker.getContainer(container.Id).inspect();
-      const stats = await docker.getContainer(container.Id).stats({ stream: false });
-
-      // 计算 CPU 使用率
-      const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
-      const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
-      const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
-      // 计算内存使用率
-      const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
-      return {
-        id: container.Id.slice(0, 12),
-        name: container.Names[0].replace(/^\//, ''),
-        image: container.Image,
-        state: containerInfo.State.Status,
-        status: container.Status,
-        cpu: cpuUsage.toFixed(2) + '%',
-        memory: memoryUsage.toFixed(2) + '%',
-        created: new Date(container.Created * 1000).toLocaleString()
-      };
-    }));
-    res.json(containerStatus);
-  } catch (error) {
-    logger.error('获取 Docker 状态时出错:', error);
-    res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
-  }
-});
-
-// API端点:重启容器
-app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const container = docker.getContainer(req.params.id);
-    await container.restart();
-    res.json({ success: true });
-  } catch (error) {
-    logger.error('重启容器失败:', error);
-    res.status(500).json({ error: '重启容器失败', details: error.message });
-  }
-});
-
-// API端点:停止容器
-app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const container = docker.getContainer(req.params.id);
-    await container.stop();
-    res.json({ success: true });
-  } catch (error) {
-    logger.error('停止容器失败:', error);
-    res.status(500).json({ error: '停止容器失败', details: error.message });
-  }
-});
-
-// API端点:获取单个容器的状态
-app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
+      // 记录简化的请求信息
+      req.skipDetailedLogging = !isErrorResponse; // 非错误请求跳过详细日志
+      logger.request(req, res, duration);
     }
-    const container = docker.getContainer(req.params.id);
-    const containerInfo = await container.inspect();
-    res.json({ state: containerInfo.State.Status });
-  } catch (error) {
-    logger.error('获取容器状态失败:', error);
-    res.status(500).json({ error: '获取容器状态失败', details: error.message });
-  }
-});
-
-// API端点:更新容器
-app.post('/api/docker/update/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const container = docker.getContainer(req.params.id);
-    const containerInfo = await container.inspect();
-    const currentImage = containerInfo.Config.Image;
-    const [imageName] = currentImage.split(':');
-    const newImage = `${imageName}:${req.body.tag}`;
-    const containerName = containerInfo.Name.slice(1);  // 去掉开头的 '/'
-
-    logger.info(`Updating container ${req.params.id} from ${currentImage} to ${newImage}`);
-    // 拉取新镜像
-    logger.info(`Pulling new image: ${newImage}`);
-    await new Promise((resolve, reject) => {
-      docker.pull(newImage, (err, stream) => {
-        if (err) return reject(err);
-        docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output));
-      });
-    });
-    // 停止旧容器
-    logger.info('Stopping old container');
-    await container.stop();
-    // 删除旧容器
-    logger.info('Removing old container');
-    await container.remove();
-    // 创建新容器
-    logger.info('Creating new container');
-    const newContainerConfig = {
-      ...containerInfo.Config,
-      Image: newImage,
-      HostConfig: containerInfo.HostConfig,
-      NetworkingConfig: {
-        EndpointsConfig: containerInfo.NetworkSettings.Networks
-      }
-    };
-    const newContainer = await docker.createContainer({
-      ...newContainerConfig,
-      name: containerName
-    });
-    // 启动新容器
-    logger.info('Starting new container');
-    await newContainer.start();
-
-    logger.success('Container update completed successfully');
-    res.json({ success: true, message: '容器更新成功' });
-  } catch (error) {
-    logger.error('更新容器失败:', error);
-    res.status(500).json({ error: '更新容器失败', details: error.message, stack: error.stack });
-  }
-});
-
-// API端点:获取容器日志
-app.get('/api/docker/logs/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const container = docker.getContainer(req.params.id);
-    const logs = await container.logs({
-      stdout: true,
-      stderr: true,
-      tail: 100,  // 获取最后100行日志
-      follow: false
-    });
-    res.send(logs);
-  } catch (error) {
-    logger.error('获取容器日志失败:', error);
-    res.status(500).json({ error: '获取容器日志失败', details: error.message });
-  }
-});
-
-const server = http.createServer(app);
-const wss = new WebSocket.Server({ server });
-
-wss.on('connection', async (ws, req) => {
-  const containerId = req.url.split('/').pop();
-  const docker = await initDocker();
-  if (!docker) {
-    ws.send('Error: 无法连接到 Docker 守护进程');
-    return;
-  }
-  const container = docker.getContainer(containerId);
-  container.logs({
-    follow: true,
-    stdout: true,
-    stderr: true,
-    tail: 100
-  }, (err, stream) => {
-    if (err) {
-      ws.send('Error: ' + err.message);
-      return;
-    }
-
-    stream.on('data', (chunk) => {
-      // 移除 ANSI 转义序列
-      const cleanedChunk = chunk.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
-      // 移除不可打印字符
-      const printableChunk = cleanedChunk.replace(/[^\x20-\x7E\x0A\x0D]/g, '');
-      ws.send(printableChunk);
-    });
-
-    ws.on('close', () => {
-      stream.destroy();
-    });
-  });
-});
-
-// API端点:删除容器
-app.post('/api/docker/delete/:id', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const container = docker.getContainer(req.params.id);
-    // 首先停止容器(如果正在运行)
-    try {
-      await container.stop();
-    } catch (stopError) {
-      logger.info('Container may already be stopped:', stopError.message);
-    }
-    
-    // 然后删除容器
-    await container.remove();
-    res.json({ success: true, message: '容器已成功删除' });
-  } catch (error) {
-    logger.error('删除容器失败:', error);
-    res.status(500).json({ error: '删除容器失败', details: error.message });
-  }
-});
-
-app.get('/api/docker/logs-poll/:id', async (req, res) => {
-  const { id } = req.params;
-  try {
-    const container = docker.getContainer(id);
-    const logs = await container.logs({
-      stdout: true,
-      stderr: true,
-      tail: 100,  // 获取最后100行日志
-      follow: false
-    });
-    res.send(logs);
-  } catch (error) {
-    res.status(500).send('获取日志失败');
-  }
-});
-
-// 网络测试
-const { execSync } = require('child_process');
-const pingPath = execSync('which ping').toString().trim();
-const traceroutePath = execSync('which traceroute').toString().trim();
-
-app.post('/api/network-test', requireLogin, (req, res) => {
-  const { type, domain } = req.body;
-  let command;
-
-  switch (type) {
-      case 'ping':
-          command = `${pingPath} -c 4 ${domain}`;
-          break;
-      case 'traceroute':
-          command = `${traceroutePath}  -m 10 ${domain}`;
-          break;
-      default:
-          return res.status(400).send('无效的测试类型');
-  }
-
-  exec(command, { timeout: 30000 }, (error, stdout, stderr) => {
-      if (error) {
-          logger.error(`执行出错: ${error}`);
-          return res.status(500).send('测试执行失败');
-      }
-      res.send(stdout || stderr);
   });
+  
+  next();
 });
 
-// docker 监控
-app.get('/api/monitoring-config', requireLogin, async (req, res) => {
-  try {
-    const config = await readConfig();
-    res.json({
-      notificationType: config.monitoringConfig.notificationType || 'wechat',
-      webhookUrl: config.monitoringConfig.webhookUrl,
-      telegramToken: config.monitoringConfig.telegramToken,
-      telegramChatId: config.monitoringConfig.telegramChatId,
-      monitorInterval: config.monitoringConfig.monitorInterval,
-      isEnabled: config.monitoringConfig.isEnabled
-    });
-  } catch (error) {
-    logger.error('Failed to get monitoring config:', error);
-    res.status(500).json({ error: 'Failed to get monitoring config', details: error.message });
-  }
-});
-
-app.post('/api/monitoring-config', requireLogin, async (req, res) => {
-  try {
-    const { notificationType, webhookUrl, telegramToken, telegramChatId, monitorInterval, isEnabled } = req.body;
-    const config = await readConfig();
-    config.monitoringConfig = { 
-      notificationType,
-      webhookUrl,
-      telegramToken,
-      telegramChatId,
-      monitorInterval: parseInt(monitorInterval), 
-      isEnabled
-    };
-    await writeConfig(config);
-
-    // 重新启动监控
-    await startMonitoring();
-
-    res.json({ success: true });
-  } catch (error) {
-    logger.error('Failed to save monitoring config:', error);
-    res.status(500).json({ error: 'Failed to save monitoring config', details: error.message });
-  }
-});
-
-// 发送告警的函数,包含重试逻辑
-async function sendAlertWithRetry(containerName, status, monitoringConfig, maxRetries = 6) {
-  const { notificationType, webhookUrl, telegramToken, telegramChatId } = monitoringConfig;
-  const cleanContainerName = containerName.replace(/^\//, '');
-  for (let attempt = 1; attempt <= maxRetries; attempt++) {
-    try {
-      if (notificationType === 'wechat') {
-        await sendWechatAlert(webhookUrl, cleanContainerName, status);
-      } else if (notificationType === 'telegram') {
-        await sendTelegramAlert(telegramToken, telegramChatId, cleanContainerName, status);
-      }
-      logger.success(`告警发送成功: ${cleanContainerName} ${status}`);
-      return;
-    } catch (error) {
-      if (attempt === maxRetries) {
-        logger.error(`达到最大重试次数,放弃发送告警: ${cleanContainerName} ${status}`);
-        return;
-      }
-      await new Promise(resolve => setTimeout(resolve, 10000));
-    }
-  }
-}
-
-async function sendWechatAlert(webhookUrl, containerName, status) {
-  const response = await axios.post(webhookUrl, {
-    msgtype: 'text',
-    text: {
-      content: `通知: 容器 ${containerName} ${status}`
-    }
-  }, {
-    timeout: 5000
-  });
-
-  if (response.status !== 200 || response.data.errcode !== 0) {
-    throw new Error(`请求成功但返回错误:${response.data.errmsg}`);
-  }
-}
-
-async function sendTelegramAlert(token, chatId, containerName, status) {
-  const url = `https://api.telegram.org/bot${token}/sendMessage`;
-  const response = await axios.post(url, {
-    chat_id: chatId,
-    text: `通知: 容器 ${containerName} ${status}`
-  }, {
-    timeout: 5000
-  });
-
-  if (response.status !== 200 || !response.data.ok) {
-    throw new Error(`发送Telegram消息失败:${JSON.stringify(response.data)}`);
-  }
-}
-
-app.post('/api/test-notification', requireLogin, async (req, res) => {
-  try {
-    const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body;
-    
-    if (notificationType === 'wechat') {
-      await sendWechatAlert(webhookUrl, 'Test Container', 'This is a test notification');
-    } else if (notificationType === 'telegram') {
-      await sendTelegramAlert(telegramToken, telegramChatId, 'Test Container', 'This is a test notification');
-    } else {
-      throw new Error('Unsupported notification type');
-    }
-    res.json({ success: true, message: 'Test notification sent successfully' });
-  } catch (error) {
-    logger.error('Failed to send test notification:', error);
-    res.status(500).json({ error: 'Failed to send test notification', details: error.message });
-  }
-});
-
-let monitoringInterval; // 用于跟踪监控间隔
-let sentAlerts = new Set(); // 用于跟踪已发送的告警
-
-async function startMonitoring() {
-  const config = await readConfig();
-  const { notificationType, webhookUrl, telegramToken, telegramChatId, monitorInterval, isEnabled } = config.monitoringConfig || {};
-
-  if (isEnabled) {
-    const docker = await initDocker();
-    if (docker) {
-      await initializeContainerStates(docker);
-      await checkContainerStates(docker, config.monitoringConfig);
-      if (monitoringInterval) {
-        clearInterval(monitoringInterval);
-      }
+// 使用我们的路由注册函数加载所有路由
+logger.info('注册所有应用路由...');
+const registerRoutes = require('./routes');
+registerRoutes(app);
 
-      const dockerEventStream = await docker.getEvents();
+// 提供兼容层以确保旧接口继续工作
+require('./compatibility-layer')(app);
 
-      dockerEventStream.on('data', async (chunk) => {
-        const event = JSON.parse(chunk.toString());
-        if (event.Type === 'container' && (event.Action === 'start' || event.Action === 'die')) {
-          await handleContainerEvent(docker, event, config.monitoringConfig);
-        }
-      });
-
-      monitoringInterval = setInterval(async () => {
-        await checkContainerStates(docker, config.monitoringConfig);
-      }, (monitorInterval || 60) * 1000);
-    }
-  } else if (monitoringInterval) {
-    clearInterval(monitoringInterval);
-    monitoringInterval = null;
-  }
-}
-
-async function initializeContainerStates(docker) {
-  const containers = await docker.listContainers({ all: true });
-  for (const container of containers) {
-    const containerInfo = await docker.getContainer(container.Id).inspect();
-    containerStates.set(container.Id, containerInfo.State.Status);
-  }
-}
-
-async function handleContainerEvent(docker, event, monitoringConfig) {
-  const containerId = event.Actor.ID;
-  const container = docker.getContainer(containerId);
-  const containerInfo = await container.inspect();
-  const newStatus = containerInfo.State.Status;
-  const oldStatus = containerStates.get(containerId);
-
-  if (oldStatus && oldStatus !== newStatus) {
-    if (newStatus === 'running') {
-      await sendAlertWithRetry(containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
-      lastStopAlertTime.delete(containerInfo.Name);
-      secondAlertSent.delete(containerInfo.Name);
-    } else if (oldStatus === 'running') {
-      await sendAlertWithRetry(containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
-      lastStopAlertTime.set(containerInfo.Name, Date.now());
-      secondAlertSent.delete(containerInfo.Name);
-    }
-    containerStates.set(containerId, newStatus);
-  }
-}
-
-async function checkContainerStates(docker, monitoringConfig) {
-  const containers = await docker.listContainers({ all: true });
-  for (const container of containers) {
-    const containerInfo = await docker.getContainer(container.Id).inspect();
-    const newStatus = containerInfo.State.Status;
-    const oldStatus = containerStates.get(container.Id);
-    
-    if (oldStatus && oldStatus !== newStatus) {
-      if (newStatus === 'running') {
-        await sendAlertWithRetry(containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
-        lastStopAlertTime.delete(containerInfo.Name);
-        secondAlertSent.delete(containerInfo.Name);
-      } else if (oldStatus === 'running') {
-        await sendAlertWithRetry(containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
-        lastStopAlertTime.set(containerInfo.Name, Date.now());
-        secondAlertSent.delete(containerInfo.Name);
-      }
-      containerStates.set(container.Id, newStatus);
-    } else if (newStatus !== 'running') {
-      await checkSecondStopAlert(containerInfo.Name, newStatus, monitoringConfig);
-    }
-  }
-}
-
-async function checkSecondStopAlert(containerName, currentStatus, monitoringConfig) {
-  const now = Date.now();
-  const lastStopAlert = lastStopAlertTime.get(containerName) || 0;
-  // 如果距离上次停止告警超过1小时,且还没有发送过第二次告警,则发送第二次告警
-  if (now - lastStopAlert >= 60 * 60 * 1000 && !secondAlertSent.has(containerName)) {
-    await sendAlertWithRetry(containerName, `仍未恢复 (当前状态: ${currentStatus})`, monitoringConfig);
-    secondAlertSent.add(containerName); // 标记已发送第二次告警
-  }
-}
-
-async function sendAlert(webhookUrl, containerName, status) {
-  try {
-    await axios.post(webhookUrl, {
-      msgtype: 'text',
-      text: {
-        content: `告警通知: 容器 ${containerName} 当前状态为 ${status}`
-      }
-    });
-  } catch (error) {
-    logger.error('发送告警失败:', error);
-  }
+// 确保登录路由可用
+try {
+  const loginRouter = require('./routes/login');
+  app.use('/api', loginRouter);
+  logger.success('✓ 已添加备用登录路由');
+} catch (loginError) {
+  logger.error('无法加载备用登录路由:', loginError);
 }
 
-// API端点:切换监控状态
-app.post('/api/toggle-monitoring', requireLogin, async (req, res) => {
-  try {
-    const { isEnabled } = req.body;
-    const config = await readConfig();
-    config.monitoringConfig.isEnabled = isEnabled;
-    await writeConfig(config);
-
-    if (isEnabled) {
-      await startMonitoring();
-    } else {
-      clearInterval(monitoringInterval);
-      monitoringInterval = null;
-    }
-
-    res.json({ success: true, message: `Monitoring ${isEnabled ? 'enabled' : 'disabled'}` });
-  } catch (error) {
-    logger.error('Failed to toggle monitoring:', error);
-    res.status(500).json({ error: 'Failed to toggle monitoring', details: error.message });
-  }
+// 页面路由
+app.get('/', (req, res) => {
+  res.sendFile(path.join(__dirname, 'web', 'index.html'));
 });
 
-app.get('/api/stopped-containers', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const containers = await docker.listContainers({ all: true });
-    const stoppedContainers = containers
-      .filter(container => container.State !== 'running')
-      .map(container => ({
-        id: container.Id.slice(0, 12),
-        name: container.Names[0].replace(/^\//, ''),
-        status: container.State
-      }));
-    res.json(stoppedContainers);
-  } catch (error) {
-    logger.error('获取已停止容器列表失败:', error);
-    res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
-  }
+app.get('/admin', (req, res) => {
+  res.sendFile(path.join(__dirname, 'web', 'admin.html'));
 });
 
-app.get('/api/refresh-stopped-containers', requireLogin, async (req, res) => {
-  try {
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    const containers = await docker.listContainers({ all: true });
-    const stoppedContainers = containers
-      .filter(container => container.State !== 'running')
-      .map(container => ({
-        id: container.Id.slice(0, 12),
-        name: container.Names[0].replace(/^\//, ''),
-        status: container.State
-      }));
-    res.json(stoppedContainers);
-  } catch (error) {
-    logger.error('刷新已停止容器列表失败:', error);
-    res.status(500).json({ error: '刷新已停止容器列表失败', details: error.message });
-  }
+app.get('/docs', (req, res) => {
+  res.sendFile(path.join(__dirname, 'web', 'docs.html'));
 });
 
-// 导出函数以供其他模块使用
-module.exports = {
-  startMonitoring,
-  sendAlertWithRetry
-};
+// 废弃的登录页面路由 - 该路由未使用且导致404错误,现已移除
+// app.get('/login', (req, res) => {
+//   // 检查用户是否已登录
+//   if (req.session && req.session.user) {
+//     return res.redirect('/admin'); // 已登录用户重定向到管理页面
+//   }
+//   
+//   res.sendFile(path.join(__dirname, 'web', 'login.html'));
+// });
 
-// 退出登录API
-app.post('/api/logout', (req, res) => {
-  req.session.destroy(err => {
-    if (err) {
-      logger.error('销毁会话失败:', err);
-      return res.status(500).json({ error: 'Failed to logout' });
-    }
-    res.clearCookie('connect.sid');
-    logger.info('用户已退出登录');
-    res.json({ success: true });
-  });
+// 404处理
+app.use((req, res) => {
+  res.status(404).json({ error: '请求的资源不存在' });
 });
 
-// 模拟系统状态API(如果实际不需要实现,可以移除)
-app.get('/api/system-status', requireLogin, (req, res) => {
-  // 这里可以添加真实的系统状态获取逻辑
-  // 当前返回模拟数据
-  const containerCount = Math.floor(Math.random() * 10) + 1;
-  const memoryUsage = Math.floor(Math.random() * 30) + 40 + '%';
-  const cpuLoad = Math.floor(Math.random() * 25) + 20 + '%';
-  const diskSpace = Math.floor(Math.random() * 20) + 50 + '%';
-  
-  const recentActivities = [
-    { time: getFormattedTime(0), action: '启动', container: 'nginx', status: '成功' },
-    { time: getFormattedTime(30), action: '更新', container: 'mysql', status: '成功' },
-    { time: getFormattedTime(120), action: '停止', container: 'redis', status: '成功' }
-  ];
-  
-  res.json({
-    containerCount,
-    memoryUsage,
-    cpuLoad,
-    diskSpace,
-    recentActivities
-  });
+// 错误处理中间件
+app.use((err, req, res, next) => {
+  logger.error('应用错误:', err);
+  res.status(500).json({ error: '服务器内部错误', details: err.message });
 });
 
-// 获取格式化的时间(例如:"今天 15:30")
-function getFormattedTime(minutesAgo) {
-  const date = new Date(Date.now() - minutesAgo * 60 * 1000);
-  const hours = date.getHours().toString().padStart(2, '0');
-  const minutes = date.getMinutes().toString().padStart(2, '0');
-  
-  if (minutesAgo < 24 * 60) {
-    return `今天 ${hours}:${minutes}`;
-  } else {
-    return `昨天 ${hours}:${minutes}`;
-  }
-}
-
-// 用户统计API
-app.get('/api/user-stats', requireLogin, (req, res) => {
-  // 模拟数据
-  res.json({
-    loginCount: Math.floor(Math.random() * 20) + 1,
-    lastLogin: getFormattedTime(Math.floor(Math.random() * 48 * 60)),
-    accountAge: Math.floor(Math.random() * 100) + 1
-  });
-});
+// 启动服务器
+const PORT = process.env.PORT || 3000;
 
-// 获取真实系统状态的API
-app.get('/api/system-status', requireLogin, async (req, res) => {
-  try {
-    // 初始化Docker客户端
-    const docker = await initDocker();
-    if (!docker) {
-      return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
-    }
-    
-    // 获取容器数量
-    const containers = await docker.listContainers({ all: true });
-    const runningContainers = containers.filter(c => c.State === 'running');
-    const containerCount = containers.length;
-    
-    // 获取系统信息,使用child_process执行系统命令
-    let memoryUsage = '未知';
-    let cpuLoad = '未知';
-    let diskSpace = '未知';
+async function startServer() {
+  server.listen(PORT, async () => {
+    logger.info(`服务器已启动并监听端口 ${PORT}`);
     
     try {
-      // 获取内存使用情况
-      const memInfo = await execPromise('free -m | grep Mem');
-      const memParts = memInfo.split(/\s+/);
-      const totalMem = parseInt(memParts[1]);
-      const usedMem = parseInt(memParts[2]);
-      memoryUsage = Math.round((usedMem / totalMem) * 100) + '%';
+      // 确保目录存在
+      await ensureDirectoriesExist();
+      logger.success('系统目录初始化完成');
       
-      // 获取CPU负载
-      const loadInfo = await execPromise('cat /proc/loadavg');
-      const loadParts = loadInfo.split(' ');
-      cpuLoad = loadParts[0];
+      // 下载必要资源
+      await downloadImages();
+      logger.success('资源下载完成');
       
-      // 获取磁盘空间
-      const diskInfo = await execPromise('df -h | grep -E "/$|/home"');
-      const diskParts = diskInfo.split(/\s+/);
-      diskSpace = diskParts[4]; // 使用百分比
-    } catch (err) {
-      logger.error('获取系统信息时出错:', err);
-      
-      // 如果获取系统信息失败,尝试使用 Docker 的统计信息
+      // 初始化系统
       try {
-        const stats = await Promise.all(runningContainers.map(c => 
-          docker.getContainer(c.Id).stats({ stream: false })
-        ));
-        
-        // 计算平均CPU使用率
-        const avgCpuUsage = stats.reduce((acc, stat) => {
-          const cpuDelta = stat.cpu_stats.cpu_usage.total_usage - stat.precpu_stats.cpu_usage.total_usage;
-          const systemDelta = stat.cpu_stats.system_cpu_usage - stat.precpu_stats.system_cpu_usage;
-          const usage = (cpuDelta / systemDelta) * stat.cpu_stats.online_cpus * 100;
-          return acc + usage;
-        }, 0) / (stats.length || 1);
-        
-        // 计算总内存使用率
-        const totalMemoryUsage = stats.reduce((acc, stat) => {
-          const usage = stat.memory_stats.usage / stat.memory_stats.limit * 100;
-          return acc + usage;
-        }, 0) / (stats.length || 1);
-        
-        cpuLoad = avgCpuUsage.toFixed(2) + '%';
-        memoryUsage = totalMemoryUsage.toFixed(2) + '%';
-      } catch (statsErr) {
-        logger.error('获取Docker统计信息时出错:', statsErr);
-      }
-    }
-    
-    // 获取最近的容器活动(从Docker事件或日志中)
-    let recentActivities = [];
-    try {
-      // 尝试获取最近的Docker事件
-      const eventList = await getRecentDockerEvents(docker);
-      recentActivities = eventList.slice(0, 10).map(event => ({
-        time: new Date(event.time * 1000).toLocaleString(),
-        action: event.Action,
-        container: event.Actor?.Attributes?.name || '未知容器',
-        status: event.status || '完成'
-      }));
-    } catch (eventsErr) {
-      logger.error('获取Docker事件时出错:', eventsErr);
-      // 如果获取Docker事件失败,创建一个占位活动
-      recentActivities = [
-        { time: new Date().toLocaleString(), action: '系统', container: '监控服务', status: '活动' }
-      ];
-    }
-    
-    res.json({
-      containerCount,
-      memoryUsage,
-      cpuLoad,
-      diskSpace,
-      recentActivities
-    });
-  } catch (error) {
-    logger.error('获取系统状态失败:', error);
-    res.status(500).json({ error: '获取系统状态失败', details: error.message });
-  }
-});
-
-// 获取磁盘空间信息的辅助API
-app.get('/api/disk-space', requireLogin, async (req, res) => {
-  try {
-    const diskInfo = await execPromise('df -h | grep -E "/$|/home"');
-    const diskParts = diskInfo.split(/\s+/);
-    
-    res.json({
-      diskSpace: diskParts[2] + '/' + diskParts[1], // 已用/总量
-      usagePercent: parseInt(diskParts[4].replace('%', '')) // 使用百分比
-    });
-  } catch (error) {
-    logger.error('获取磁盘空间信息失败:', error);
-    res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message });
-  }
-});
-
-// 用户统计API - 使用真实数据
-app.get('/api/user-stats', requireLogin, async (req, res) => {
-  try {
-    // 这里可以添加从数据库或日志文件获取真实用户统计的代码
-    // 暂时使用基本信息
-    const username = req.session.user.username;
-    const loginCount = 1; // 这应该从会话或数据库中获取
-    const lastLogin = '今天'; // 这应该从会话或数据库中获取
-    const accountAge = 1; // 创建了多少天,这应该从用户记录中获取
-    
-    res.json({
-      username,
-      loginCount,
-      lastLogin,
-      accountAge
-    });
-  } catch (error) {
-    logger.error('获取用户统计信息失败:', error);
-    res.status(500).json({
-      loginCount: 1,
-      lastLogin: '今天',
-      accountAge: 1
-    });
-  }
-});
-
-// Promise化的exec
-function execPromise(command) {
-  return new Promise((resolve, reject) => {
-    exec(command, (error, stdout, stderr) => {
-      if (error) {
-        reject(error);
-        return;
-      }
-      resolve(stdout.trim());
-    });
-  });
-}
-
-// 获取最近的Docker事件
-async function getRecentDockerEvents(docker) {
-  try {
-    // 注意:Dockerode目前的getEvents API可能不支持历史事件查询
-    // 这是一个模拟实现,实际使用时可能需要适当调整
-    const sinceTime = Math.floor(Date.now() / 1000) - 3600; // 一小时前
-    
-    return [
-      {
-        time: Math.floor(Date.now() / 1000) - 60,
-        Action: '启动',
-        Actor: { Attributes: { name: 'nginx' } },
-        status: '成功'
-      },
-      {
-        time: Math.floor(Date.now() / 1000) - 180,
-        Action: '重启',
-        Actor: { Attributes: { name: 'mysql' } },
-        status: '成功'
-      },
-      {
-        time: Math.floor(Date.now() / 1000) - 360,
-        Action: '更新',
-        Actor: { Attributes: { name: 'redis' } },
-        status: '成功'
+        const { initialize } = require('./scripts/init-system');
+        await initialize();
+        logger.success('系统初始化完成');
+      } catch (initError) {
+        logger.warn('系统初始化遇到问题:', initError.message);
+        logger.warn('某些功能可能无法正常工作');
       }
-    ];
-  } catch (error) {
-    logger.error('获取Docker事件失败:', error);
-    return [];
-  }
-}
-
-app.get('/api/system-stats', requireLogin, async (req, res) => {
-  try {
-    let dockerAvailable = false;
-    let containerCount = '0';
-    let memoryUsage = '0%';
-    let cpuLoad = '0%';
-    let diskSpace = '0%';
-    let recentActivities = [];
-
-    // 尝试初始化Docker
-    const docker = await initDocker();
-    if (docker) {
-      dockerAvailable = true;
       
-      // 获取容器统计
+      // 尝试启动监控
       try {
-        const containers = await docker.listContainers({ all: true });
-        containerCount = containers.length.toString();
-        
-        // 获取最近的容器活动
-        const runningContainers = containers.filter(c => c.State === 'running');
-        for (let i = 0; i < Math.min(3, runningContainers.length); i++) {
-          recentActivities.push({
-            time: new Date(runningContainers[i].Created * 1000).toLocaleString(),
-            action: '运行中',
-            container: runningContainers[i].Names[0].replace(/^\//, ''),
-            status: '正常'
-          });
-        }
-      } catch (containerError) {
-        logger.error('获取容器信息失败:', containerError);
-      }
-    }
-    
-    // 即使Docker不可用,也尝试获取系统信息
-    try {
-      // 获取内存使用情况
-      const memInfo = await execPromise('free -m | grep Mem');
-      if (memInfo) {
-        const memParts = memInfo.split(/\s+/);
-        if (memParts.length >= 3) {
-          const total = parseInt(memParts[1], 10);
-          const used = parseInt(memParts[2], 10);
-          memoryUsage = Math.round((used / total) * 100) + '%';
-        }
+        const monitoringService = require('./services/monitoringService');
+        await monitoringService.startMonitoring();
+        logger.success('监控服务已启动');
+      } catch (monitoringError) {
+        logger.warn('监控服务启动失败:', monitoringError.message);
+        logger.warn('监控功能可能不可用');
       }
       
-      // 获取CPU负载
-      const loadAvg = await execPromise('cat /proc/loadavg');
-      if (loadAvg) {
-        const load = parseFloat(loadAvg.split(' ')[0]);
-        cpuLoad = (load * 100).toFixed(2) + '%';
-      }
-      
-      // 获取磁盘空间
-      const diskInfo = await execPromise('df -h | grep -E "/$|/home" | head -1');
-      if (diskInfo) {
-        const diskParts = diskInfo.split(/\s+/);
-        if (diskParts.length >= 5) {
-          diskSpace = diskParts[4]; // 使用百分比
+      // 尝试设置WebSocket
+      try {
+        const dockerRouter = require('./routes/docker');
+        if (typeof dockerRouter.setupLogWebsocket === 'function') {
+          dockerRouter.setupLogWebsocket(server);
+          logger.success('WebSocket服务已启动');
         }
+      } catch (wsError) {
+        logger.warn('WebSocket服务启动失败:', wsError.message);
+        logger.warn('容器日志实时流可能不可用');
       }
-    } catch (sysError) {
-      logger.error('获取系统信息失败:', sysError);
-    }
-    
-    // 如果没有活动记录,添加一个默认记录
-    if (recentActivities.length === 0) {
-      recentActivities.push({
-        time: new Date().toLocaleString(),
-        action: '系统检查',
-        container: '监控服务',
-        status: dockerAvailable ? '正常' : 'Docker服务不可用'
-      });
+      
+      logger.success('服务器初始化完成,系统已准备就绪');
+    } catch (error) {
+      logger.error('系统初始化失败,但服务仍将继续运行:', error);
     }
-    
-    // 返回收集到的所有数据,即使部分数据可能不完整
-    res.json({
-      dockerAvailable,
-      containerCount,
-      memoryUsage,
-      cpuLoad,
-      diskSpace,
-      recentActivities
-    });
-    
-  } catch (error) {
-    logger.error('获取系统统计数据失败:', error);
-    
-    // 即使出错,仍然尝试返回一些基本数据
-    res.status(200).json({
-      dockerAvailable: false,
-      containerCount: '0',
-      memoryUsage: '未知',
-      cpuLoad: '未知',
-      diskSpace: '未知',
-      recentActivities: [{
-        time: new Date().toLocaleString(),
-        action: '系统错误',
-        container: '监控服务',
-        status: '数据获取失败'
-      }],
-      error: '获取系统统计数据失败',
-      errorDetails: error.message
-    });
-  }
-});
-
-
-// 辅助函数
-function execPromise(cmd) {
-  return new Promise((resolve, reject) => {
-    exec(cmd, (error, stdout, stderr) => {
-      if (error) {
-        reject(error);
-        return;
-      }
-      resolve(stdout.trim());
-    });
   });
 }
 
-// 执行系统命令的辅助函数
-async function execCommand(command) {
-  return new Promise((resolve, reject) => {
-    exec(command, (error, stdout, stderr) => {
-      if (error) {
-        reject(error);
-        return;
-      }
-      resolve({ stdout, stderr });
-    });
-  });
-}
+startServer();
 
-// API端点:获取用户信息
-app.get('/api/user-info', requireLogin, async (req, res) => {
-  try {
-    // 确保用户已登录
-    if (!req.session.user) {
-      return res.status(401).json({ error: '未登录' });
-    }
-    
-    const users = await readUsers();
-    const user = users.users.find(u => u.username === req.session.user.username);
-    
-    if (!user) {
-      return res.status(404).json({ error: '用户不存在' });
-    }
-    
-    // 计算账户年龄(如果有创建日期)
-    let accountAge = '0';
-    if (user.createdAt) {
-      const createdDate = new Date(user.createdAt);
-      const currentDate = new Date();
-      const diffTime = Math.abs(currentDate - createdDate);
-      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
-      accountAge = diffDays.toString();
-    }
-    
-    // 返回用户信息
-    res.json({
-      username: user.username,
-      loginCount: user.loginCount || '0',
-      lastLogin: user.lastLogin || '无记录',
-      accountAge
-    });
-  } catch (error) {
-    logger.error('获取用户信息失败:', error);
-    res.status(500).json({ error: '获取用户信息失败', details: error.message });
+// 处理进程终止信号
+process.on('SIGINT', gracefulShutdown);
+process.on('SIGTERM', gracefulShutdown);
+
+// 捕获未处理的Promise拒绝和未捕获的异常
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error('未处理的Promise拒绝:', reason);
+  if (reason instanceof Error) {
+    logger.debug('拒绝原因堆栈:', reason.stack);
   }
 });
 
-// 启动服务器
-const PORT = process.env.PORT || 3000;
-server.listen(PORT, async () => {
-  logger.info(`Server is running on http://localhost:${PORT}`);
-  try {
-    await startMonitoring();
-  } catch (error) {
-    logger.error('Failed to start monitoring:', error);
-  }
+process.on('uncaughtException', (error) => {
+  logger.error('未捕获的异常:', error);
+  logger.error('错误堆栈:', error.stack);
+  // 给日志一些时间写入后退出
+  setTimeout(() => {
+    logger.fatal('由于未捕获的异常,系统将在3秒后退出');
+    setTimeout(() => process.exit(1), 3000);
+  }, 1000);
 });
 
-// 统一的错误处理函数
-function handleAxiosError(error, res, message) {
-  let errorDetails = '';
-  
-  if (error.response) {
-    // 服务器响应错误
-    const status = error.response.status;
-    errorDetails = `状态码: ${status}`;
-    
-    if (error.response.data && error.response.data.message) {
-      errorDetails += `, 信息: ${error.response.data.message}`;
-    }
-    
-    console.error(`[ERROR] ${message}: ${errorDetails}`);
-    res.status(status).json({
-      error: `${message} (${errorDetails})`,
-      details: error.response.data
-    });
-    
-  } else if (error.request) {
-    // 请求已发送但没有收到响应
-    if (error.code === 'ECONNRESET') {
-      errorDetails = '连接被重置,这可能是由于网络不稳定或服务端断开连接';
-    } else if (error.code === 'ECONNABORTED') {
-      errorDetails = '请求超时,服务器响应时间过长';
-    } else {
-      errorDetails = `${error.code || '未知错误代码'}: ${error.message}`;
-    }
-    
-    console.error(`[ERROR] ${message}: ${errorDetails}`);
-    res.status(503).json({
-      error: `${message} (${errorDetails})`,
-      retryable: true
-    });
-    
-  } else {
-    // 其他错误
-    errorDetails = error.message;
-    console.error(`[ERROR] ${message}: ${errorDetails}`);
-    console.error(`[ERROR] 错误堆栈: ${error.stack}`);
-    
-    res.status(500).json({
-      error: `${message} (${errorDetails})`,
-      retryable: true
-    });
-  }
-}
+// 导出服务器对象以供测试使用
+module.exports = server;

+ 47 - 0
hubcmdui/services/configService.js

@@ -0,0 +1,47 @@
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('../logger');
+
+const CONFIG_FILE = path.join(__dirname, '../config.json');
+const DEFAULT_CONFIG = {
+  theme: 'light',
+  language: 'zh_CN',
+  notifications: true,
+  autoRefresh: true,
+  refreshInterval: 30000,
+  dockerHost: 'localhost',
+  dockerPort: 2375,
+  useHttps: false
+};
+
+async function ensureConfigFile() {
+  try {
+    await fs.access(CONFIG_FILE);
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      await fs.writeFile(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
+    } else {
+      throw error;
+    }
+  }
+}
+
+async function getConfig() {
+  try {
+    await ensureConfigFile();
+    const data = await fs.readFile(CONFIG_FILE, 'utf8');
+    return JSON.parse(data);
+  } catch (error) {
+    logger.error('读取配置文件失败:', error);
+    return { ...DEFAULT_CONFIG, error: true };
+  }
+}
+
+module.exports = {
+  getConfig,
+  saveConfig: async (config) => {
+    await ensureConfigFile();
+    await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
+  },
+  DEFAULT_CONFIG
+};

+ 290 - 0
hubcmdui/services/dockerHubService.js

@@ -0,0 +1,290 @@
+/**
+ * Docker Hub 服务模块
+ */
+const axios = require('axios');
+const logger = require('../logger');
+const pLimit = require('p-limit');
+const axiosRetry = require('axios-retry');
+
+// 配置并发限制,最多5个并发请求
+const limit = pLimit(5);
+
+// 优化HTTP请求配置
+const httpOptions = {
+  timeout: 15000, // 15秒超时
+  headers: {
+    'User-Agent': 'DockerHubSearchClient/1.0',
+    'Accept': 'application/json'
+  }
+};
+
+// 配置Axios重试
+axiosRetry(axios, {
+  retries: 3, // 最多重试3次
+  retryDelay: (retryCount) => {
+    console.log(`[INFO] 重试 Docker Hub 请求 (${retryCount}/3)`);
+    return retryCount * 1000; // 重试延迟,每次递增1秒
+  },
+  retryCondition: (error) => {
+    // 只在网络错误或5xx响应时重试
+    return axiosRetry.isNetworkOrIdempotentRequestError(error) || 
+           (error.response && error.response.status >= 500);
+  }
+});
+
+// 搜索仓库
+async function searchRepositories(term, page = 1, requestCache = null) {
+  const cacheKey = `search_${term}_${page}`;
+  let cachedResult = null;
+  
+  // 安全地检查缓存
+  if (requestCache && typeof requestCache.get === 'function') {
+    cachedResult = requestCache.get(cacheKey);
+  }
+  
+  if (cachedResult) {
+    console.log(`[INFO] 返回缓存的搜索结果: ${term} (页码: ${page})`);
+    return cachedResult;
+  }
+  
+  console.log(`[INFO] 搜索Docker Hub: ${term} (页码: ${page})`);
+  
+  try {
+    // 使用更安全的直接请求方式,避免pLimit可能的问题
+    const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`;
+    const response = await axios.get(url, httpOptions);
+    const result = response.data;
+    
+    // 将结果缓存(如果缓存对象可用)
+    if (requestCache && typeof requestCache.set === 'function') {
+      requestCache.set(cacheKey, result);
+    }
+    
+    return result;
+  } catch (error) {
+    logger.error('搜索Docker Hub失败:', error.message);
+    // 重新抛出错误以便上层处理
+    throw new Error(error.message || '搜索Docker Hub失败');
+  }
+}
+
+// 获取所有标签
+async function getAllTags(imageName, isOfficial) {
+  const fullImageName = isOfficial ? `library/${imageName}` : imageName;
+  logger.info(`获取所有镜像标签: ${fullImageName}`);
+  
+  // 为所有标签请求设置超时限制
+  const allTagsPromise = fetchAllTags(fullImageName);
+  const timeoutPromise = new Promise((_, reject) => 
+    setTimeout(() => reject(new Error('获取所有标签超时')), 30000)
+  );
+  
+  try {
+    // 使用Promise.race确保请求不会无限等待
+    const allTags = await Promise.race([allTagsPromise, timeoutPromise]);
+    
+    // 过滤掉无效平台信息
+    const cleanedTags = allTags.map(tag => {
+      if (tag.images && Array.isArray(tag.images)) {
+        tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
+      }
+      return tag;
+    });
+    
+    return {
+      count: cleanedTags.length,
+      results: cleanedTags,
+      all_pages_loaded: true
+    };
+  } catch (error) {
+    logger.error(`获取所有标签失败: ${error.message}`);
+    throw error;
+  }
+}
+
+// 获取特定页的标签
+async function getTagsByPage(imageName, isOfficial, page, pageSize) {
+  const fullImageName = isOfficial ? `library/${imageName}` : imageName;
+  logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 页面大小: ${pageSize}`);
+  
+  const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`;
+  
+  try {
+    const tagsResponse = await axios.get(tagsUrl, {
+      timeout: 15000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
+      }
+    });
+    
+    // 检查响应数据有效性
+    if (!tagsResponse.data || typeof tagsResponse.data !== 'object') {
+      logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`);
+      return { count: 0, results: [] };
+    }
+    
+    if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) {
+      logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`);
+      return { count: 0, results: [] };
+    }
+    
+    // 过滤掉无效平台信息
+    const cleanedResults = tagsResponse.data.results.map(tag => {
+      if (tag.images && Array.isArray(tag.images)) {
+        tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown'));
+      }
+      return tag;
+    });
+    
+    return {
+      ...tagsResponse.data,
+      results: cleanedResults
+    };
+  } catch (error) {
+    logger.error(`获取标签列表失败: ${error.message}`, {
+      url: tagsUrl,
+      status: error.response?.status,
+      statusText: error.response?.statusText
+    });
+    throw error;
+  }
+}
+
+// 获取标签数量
+async function getTagCount(name, isOfficial, requestCache) {
+  const cacheKey = `tag_count_${name}_${isOfficial}`;
+  const cachedResult = requestCache?.get(cacheKey);
+  
+  if (cachedResult) {
+    console.log(`[INFO] 返回缓存的标签计数: ${name}`);
+    return cachedResult;
+  }
+  
+  const fullImageName = isOfficial ? `library/${name}` : name;
+  const apiUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags/?page_size=1`;
+  
+  try {
+    const result = await limit(async () => {
+      const response = await axios.get(apiUrl, httpOptions);
+      return {
+        count: response.data.count,
+        recommended_mode: response.data.count > 500 ? 'paginated' : 'full'
+      };
+    });
+    
+    if (requestCache) {
+      requestCache.set(cacheKey, result);
+    }
+    
+    return result;
+  } catch (error) {
+    throw error;
+  }
+}
+
+// 递归获取所有标签
+async function fetchAllTags(fullImageName, page = 1, allTags = [], maxPages = 10) {
+  if (page > maxPages) {
+    logger.warn(`达到最大页数限制 (${maxPages}),停止获取更多标签`);
+    return allTags;
+  }
+  
+  const pageSize = 100; // 使用最大页面大小
+  const url = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`;
+  
+  try {
+    logger.info(`获取标签页 ${page}/${maxPages}...`);
+    
+    const response = await axios.get(url, {
+      timeout: 10000,
+      headers: {
+        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36'
+      }
+    });
+    
+    if (!response.data.results || !Array.isArray(response.data.results)) {
+      logger.warn(`页 ${page} 没有有效的标签数据`);
+      return allTags;
+    }
+    
+    allTags.push(...response.data.results);
+    logger.info(`已获取 ${allTags.length}/${response.data.count || 'unknown'} 个标签`);
+    
+    // 检查是否有下一页
+    if (response.data.next && allTags.length < response.data.count) {
+      // 添加一些延迟以避免请求过快
+      await new Promise(resolve => setTimeout(resolve, 500));
+      return fetchAllTags(fullImageName, page + 1, allTags, maxPages);
+    }
+    
+    logger.success(`成功获取所有 ${allTags.length} 个标签`);
+    return allTags;
+  } catch (error) {
+    logger.error(`递归获取标签失败 (页码 ${page}): ${error.message}`);
+    
+    // 如果已经获取了一些标签,返回这些标签而不是抛出错误
+    if (allTags.length > 0) {
+      return allTags;
+    }
+    
+    // 如果没有获取到任何标签,则抛出错误
+    throw error;
+  }
+}
+
+// 统一的错误处理函数
+function handleAxiosError(error, res, message) {
+  let errorDetails = '';
+  
+  if (error.response) {
+    // 服务器响应错误的错误处理函数
+    const status = error.response.status;
+    errorDetails = `状态码: ${status}`;
+    
+    if (error.response.data && error.response.data.message) {
+      errorDetails += `, 信息: ${error.response.data.message}`;
+    }
+    
+    console.error(`[ERROR] ${message}: ${errorDetails}`);
+    
+    res.status(status).json({
+      error: `${message} (${errorDetails})`,
+      details: error.response.data
+    });
+  } else if (error.request) {
+    // 请求已发送但没有收到响应
+    if (error.code === 'ECONNRESET') {
+      errorDetails = '连接被重置,这可能是由于网络不稳定或服务端断开连接';
+    } else if (error.code === 'ECONNABORTED') {
+      errorDetails = '请求超时,服务器响应时间过长';
+    } else {
+      errorDetails = `${error.code || '未知错误代码'}: ${error.message}`;
+    }
+    
+    console.error(`[ERROR] ${message}: ${errorDetails}`);
+    
+    res.status(503).json({
+      error: `${message} (${errorDetails})`,
+      retryable: true
+    });
+  } else {
+    // 其他错误
+    errorDetails = error.message;
+    console.error(`[ERROR] ${message}: ${errorDetails}`);
+    console.error(`[ERROR] 错误堆栈: ${error.stack}`);
+    
+    res.status(500).json({
+      error: `${message} (${errorDetails})`,
+      retryable: true
+    });
+  }
+}
+
+module.exports = {
+  searchRepositories,
+  getAllTags,
+  getTagsByPage,
+  getTagCount,
+  fetchAllTags,
+  handleAxiosError
+};

+ 476 - 0
hubcmdui/services/dockerService.js

@@ -0,0 +1,476 @@
+/**
+ * Docker服务模块 - 处理Docker容器管理
+ */
+const Docker = require('dockerode');
+const logger = require('../logger');
+
+let docker = null;
+
+async function initDockerConnection() {
+  if (docker) return docker;
+  
+  try {
+    // 兼容MacOS的Docker socket路径
+    const options = process.platform === 'darwin' 
+      ? { socketPath: '/var/run/docker.sock' }
+      : null;
+    
+    docker = new Docker(options);
+    await docker.ping();
+    logger.success('成功连接到Docker守护进程');
+    return docker;
+  } catch (error) {
+    logger.error('Docker连接失败:', error.message);
+    return null; // 返回null而不是抛出错误
+  }
+}
+
+// 获取Docker连接
+async function getDockerConnection() {
+  if (!docker) {
+    docker = await initDockerConnection();
+  }
+  return docker;
+}
+
+// 修改其他Docker相关方法,添加更友好的错误处理
+async function getContainersStatus() {
+  const docker = await initDockerConnection();
+  if (!docker) {
+    logger.warn('[getContainersStatus] Cannot connect to Docker daemon, returning error indicator.');
+    // 返回带有特殊错误标记的数组,前端可以通过这个标记识别 Docker 不可用
+    return [{ 
+      id: 'n/a',
+      name: 'Docker 服务不可用',
+      image: 'n/a',
+      state: 'error',
+      status: 'Docker 服务未运行或无法连接',
+      error: 'DOCKER_UNAVAILABLE', // 特殊错误标记
+      cpu: 'N/A',
+      memory: 'N/A',
+      created: new Date().toLocaleString()
+    }];
+  }
+  
+  let containers = [];
+  try {
+      containers = await docker.listContainers({ all: true });
+      logger.info(`[getContainersStatus] Found ${containers.length} containers.`);
+  } catch (listError) {
+      logger.error('[getContainersStatus] Error listing containers:', listError.message || listError);
+      // 使用同样的错误标记模式
+      return [{ 
+        id: 'n/a',
+        name: '容器列表获取失败',
+        image: 'n/a',
+        state: 'error',
+        status: `获取容器列表失败: ${listError.message}`,
+        error: 'CONTAINER_LIST_ERROR',
+        cpu: 'N/A',
+        memory: 'N/A',
+        created: new Date().toLocaleString()
+      }];
+  }
+
+  const containerPromises = containers.map(async (container) => {
+    try { 
+        const containerInspectInfo = await docker.getContainer(container.Id).inspect();
+        
+        let stats = {};
+        let cpuUsage = 'N/A';
+        let memoryUsage = 'N/A';
+
+        // 仅在容器运行时尝试获取 stats
+        if (containerInspectInfo.State.Running) {
+            try {
+                 stats = await docker.getContainer(container.Id).stats({ stream: false });
+                 
+                 // Safely calculate CPU usage
+                 if (stats.precpu_stats && stats.cpu_stats && stats.cpu_stats.cpu_usage && stats.precpu_stats.cpu_usage && stats.cpu_stats.system_cpu_usage && stats.precpu_stats.system_cpu_usage) {
+                     const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
+                     const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
+                     if (systemDelta > 0 && stats.cpu_stats.online_cpus > 0) {
+                         cpuUsage = ((cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100.0).toFixed(2) + '%';
+                     } else {
+                         cpuUsage = '0.00%'; // Handle division by zero or no change
+                     }
+                 } else {
+                      logger.warn(`[getContainersStatus] Incomplete CPU stats for container ${container.Id}`);
+                 }
+
+                 // Safely calculate Memory usage
+                 if (stats.memory_stats && stats.memory_stats.usage && stats.memory_stats.limit) {
+                     const memoryLimit = stats.memory_stats.limit;
+                     if (memoryLimit > 0) {
+                         memoryUsage = ((stats.memory_stats.usage / memoryLimit) * 100.0).toFixed(2) + '%';
+                     } else {
+                         memoryUsage = '0.00%'; // Handle division by zero (unlikely)
+                     }
+                 } else {
+                      logger.warn(`[getContainersStatus] Incomplete Memory stats for container ${container.Id}`);
+                 }
+                 
+            } catch (statsError) {
+                 logger.warn(`[getContainersStatus] Failed to get stats for running container ${container.Id}: ${statsError.message}`);
+                 // 保留 N/A 值
+            }
+        }
+        
+        return {
+          id: container.Id.slice(0, 12),
+          name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : (containerInspectInfo.Name ? containerInspectInfo.Name.replace(/^\//, '') : 'N/A'),
+          image: container.Image || 'N/A',
+          state: containerInspectInfo.State.Status || container.State || 'N/A',
+          status: container.Status || 'N/A',
+          cpu: cpuUsage,
+          memory: memoryUsage,
+          created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
+        };
+    } catch(err) {
+        logger.warn(`[getContainersStatus] Failed to get inspect info for container ${container.Id}: ${err.message}`);
+        // 返回一个包含错误信息的对象,而不是让 Promise.all 失败
+        return {
+            id: container.Id ? container.Id.slice(0, 12) : 'Unknown ID',
+            name: container.Names && container.Names.length > 0 ? container.Names[0].replace(/^\//, '') : 'Unknown Name',
+            image: container.Image || 'Unknown Image',
+            state: 'error',
+            status: `Error: ${err.message}`,
+            cpu: 'N/A',
+            memory: 'N/A',
+            created: container.Created ? new Date(container.Created * 1000).toLocaleString() : 'N/A'
+        };
+    }
+  });
+  
+  // 等待所有容器信息处理完成
+  const results = await Promise.all(containerPromises);
+  // 可以选择过滤掉完全失败的结果(虽然上面已经处理了)
+  // return results.filter(r => r.state !== 'error'); 
+  return results; // 返回所有结果,包括有错误的
+}
+
+// 获取单个容器状态
+async function getContainerStatus(id) {
+  const docker = await getDockerConnection();
+  if (!docker) {
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  const container = docker.getContainer(id);
+  const containerInfo = await container.inspect();
+  return { state: containerInfo.State.Status };
+}
+
+// 重启容器
+async function restartContainer(id) {
+  logger.info(`Attempting to restart container ${id}`);
+  const docker = await getDockerConnection();
+  if (!docker) {
+    logger.error(`[restartContainer ${id}] Cannot connect to Docker daemon.`);
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  try {
+    const container = docker.getContainer(id);
+    await container.restart();
+    logger.success(`Container ${id} restarted successfully.`);
+    return { success: true };
+  } catch (error) {
+    logger.error(`[restartContainer ${id}] Error restarting container:`, error.message || error);
+    // 检查是否是容器不存在的错误
+    if (error.statusCode === 404) {
+      throw new Error(`容器 ${id} 不存在`);
+    }
+    // 可以根据需要添加其他错误类型的检查
+    throw new Error(`重启容器失败: ${error.message}`);
+  }
+}
+
+// 停止容器
+async function stopContainer(id) {
+  logger.info(`Attempting to stop container ${id}`);
+  const docker = await getDockerConnection();
+  if (!docker) {
+    logger.error(`[stopContainer ${id}] Cannot connect to Docker daemon.`);
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  try {
+    const container = docker.getContainer(id);
+    await container.stop();
+    logger.success(`Container ${id} stopped successfully.`);
+    return { success: true };
+  } catch (error) {
+    logger.error(`[stopContainer ${id}] Error stopping container:`, error.message || error);
+    // 检查是否是容器不存在或已停止的错误
+    if (error.statusCode === 404) {
+      throw new Error(`容器 ${id} 不存在`);
+    } else if (error.statusCode === 304) {
+        logger.warn(`[stopContainer ${id}] Container already stopped.`);
+        return { success: true, message: '容器已停止' }; // 认为已停止也是成功
+    }
+    throw new Error(`停止容器失败: ${error.message}`);
+  }
+}
+
+// 删除容器
+async function deleteContainer(id) {
+  const docker = await getDockerConnection();
+  if (!docker) {
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  const container = docker.getContainer(id);
+  
+  // 首先停止容器(如果正在运行)
+  try {
+    await container.stop();
+  } catch (stopError) {
+    logger.info('Container may already be stopped:', stopError.message);
+  }
+  
+  // 然后删除容器
+  await container.remove();
+  return { success: true, message: '容器已成功删除' };
+}
+
+// 更新容器
+async function updateContainer(id, tag) {
+  const docker = await getDockerConnection();
+  if (!docker) {
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  // 获取容器信息
+  const container = docker.getContainer(id);
+  const containerInfo = await container.inspect();
+  const currentImage = containerInfo.Config.Image;
+  const [imageName] = currentImage.split(':');
+  const newImage = `${imageName}:${tag}`;
+  const containerName = containerInfo.Name.slice(1); // 去掉开头的 '/'
+  
+  logger.info(`Updating container ${id} from ${currentImage} to ${newImage}`);
+  
+  // 拉取新镜像
+  logger.info(`Pulling new image: ${newImage}`);
+  await new Promise((resolve, reject) => {
+    docker.pull(newImage, (err, stream) => {
+      if (err) return reject(err);
+      docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output));
+    });
+  });
+  
+  // 停止旧容器
+  logger.info('Stopping old container');
+  await container.stop();
+  
+  // 删除旧容器
+  logger.info('Removing old container');
+  await container.remove();
+  
+  // 创建新容器
+  logger.info('Creating new container');
+  const newContainerConfig = {
+    ...containerInfo.Config,
+    Image: newImage,
+    HostConfig: containerInfo.HostConfig,
+    NetworkingConfig: {
+      EndpointsConfig: containerInfo.NetworkSettings.Networks
+    }
+  };
+  
+  const newContainer = await docker.createContainer({
+    ...newContainerConfig,
+    name: containerName
+  });
+  
+  // 启动新容器
+  logger.info('Starting new container');
+  await newContainer.start();
+  
+  logger.success('Container update completed successfully');
+  return { success: true };
+}
+
+// 获取容器日志
+async function getContainerLogs(id, options = {}) {
+  logger.info(`Attempting to get logs for container ${id} with options:`, options);
+  const docker = await getDockerConnection();
+  if (!docker) {
+     logger.error(`[getContainerLogs ${id}] Cannot connect to Docker daemon.`);
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  try {
+    const container = docker.getContainer(id);
+    const logOptions = {
+      stdout: true,
+      stderr: true,
+      tail: options.tail || 100,
+      follow: options.follow || false
+    };
+    
+    // 修复日志获取方式
+    if (!options.follow) {
+      // 对于非流式日志,直接等待返回
+      try {
+        const logs = await container.logs(logOptions);
+        
+        // 如果logs是Buffer或字符串,直接处理
+        if (Buffer.isBuffer(logs) || typeof logs === 'string') {
+          // 清理ANSI转义码
+          const cleanedLogs = logs.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
+          logger.success(`Successfully retrieved logs for container ${id}`);
+          return cleanedLogs;
+        } 
+        // 如果logs是流,转换为字符串
+        else if (typeof logs === 'object' && logs !== null) {
+          return new Promise((resolve, reject) => {
+            let allLogs = '';
+            
+            // 处理数据事件
+            if (typeof logs.on === 'function') {
+              logs.on('data', chunk => {
+                allLogs += chunk.toString('utf8');
+              });
+              
+              logs.on('end', () => {
+                const cleanedLogs = allLogs.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
+                logger.success(`Successfully retrieved logs for container ${id}`);
+                resolve(cleanedLogs);
+              });
+              
+              logs.on('error', err => {
+                logger.error(`[getContainerLogs ${id}] Error reading log stream:`, err.message || err);
+                reject(new Error(`读取日志流失败: ${err.message}`));
+              });
+            } else {
+              // 如果不是标准流但返回了对象,尝试转换为字符串
+              logger.warn(`[getContainerLogs ${id}] Logs object does not have stream methods, trying to convert`);
+              try {
+                const logStr = logs.toString();
+                const cleanedLogs = logStr.replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
+                resolve(cleanedLogs);
+              } catch (convErr) {
+                logger.error(`[getContainerLogs ${id}] Failed to convert logs to string:`, convErr);
+                reject(new Error('日志格式转换失败'));
+              }
+            }
+          });
+        } else {
+          logger.error(`[getContainerLogs ${id}] Unexpected logs response type:`, typeof logs);
+          throw new Error('日志响应格式错误');
+        }
+      } catch (logError) {
+        logger.error(`[getContainerLogs ${id}] Error getting logs:`, logError);
+        throw logError;
+      }
+    } else {
+      // 对于流式日志,调整方式
+      logger.info(`[getContainerLogs ${id}] Returning log stream for follow=true`);
+      const stream = await container.logs(logOptions);
+      return stream; // 直接返回流对象
+    }
+  } catch (error) {
+    logger.error(`[getContainerLogs ${id}] Error getting container logs:`, error.message || error);
+    if (error.statusCode === 404) {
+        throw new Error(`容器 ${id} 不存在`);
+    }
+    throw new Error(`获取日志失败: ${error.message}`);
+  }
+}
+
+// 获取已停止的容器
+async function getStoppedContainers() {
+  const docker = await getDockerConnection();
+  if (!docker) {
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  const containers = await docker.listContainers({ 
+    all: true,
+    filters: { status: ['exited', 'dead', 'created'] }
+  });
+  
+  return containers.map(container => ({
+    id: container.Id.slice(0, 12),
+    name: container.Names[0].replace(/^\//, ''),
+    status: container.State
+  }));
+}
+
+// 获取最近的Docker事件
+async function getRecentEvents(limit = 10) {
+  const docker = await getDockerConnection();
+  if (!docker) {
+    throw new Error('无法连接到 Docker 守护进程');
+  }
+  
+  // 注意:Dockerode的getEvents API可能不支持历史事件查询
+  // 以下代码是模拟生成最近事件,实际应用中可能需要其他方式实现
+  
+  try {
+    const containers = await docker.listContainers({ 
+      all: true, 
+      limit: limit,
+      filters: { status: ['exited', 'created', 'running', 'restarting'] }
+    });
+    
+    // 从容器状态转换为事件
+    const events = containers.map(container => {
+      let action, status;
+      
+      switch(container.State) {
+        case 'running':
+          action = 'start';
+          status = '运行中';
+          break;
+        case 'exited':
+          action = 'die';
+          status = '已停止';
+          break;
+        case 'created':
+          action = 'create';
+          status = '已创建';
+          break;
+        case 'restarting':
+          action = 'restart';
+          status = '重启中';
+          break;
+        default:
+          action = 'update';
+          status = container.Status;
+      }
+      
+      return {
+        time: container.Created,
+        Action: action,
+        status: status,
+        Actor: {
+          Attributes: {
+            name: container.Names[0].replace(/^\//, '')
+          }
+        }
+      };
+    });
+    
+    return events.sort((a, b) => b.time - a.time);
+  } catch (error) {
+    logger.error('获取Docker事件失败:', error);
+    return [];
+  }
+}
+
+module.exports = {
+  initDockerConnection,
+  getDockerConnection,
+  getContainersStatus,
+  getContainerStatus,
+  restartContainer,
+  stopContainer,
+  deleteContainer,
+  updateContainer,
+  getContainerLogs,
+  getStoppedContainers,
+  getRecentEvents
+};

+ 324 - 0
hubcmdui/services/documentationService.js

@@ -0,0 +1,324 @@
+/**
+ * 文档服务模块 - 处理文档管理功能
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('../logger');
+
+const DOCUMENTATION_DIR = path.join(__dirname, '..', 'documentation');
+const META_DIR = path.join(DOCUMENTATION_DIR, 'meta');
+
+// 确保文档目录存在
+async function ensureDocumentationDir() {
+  try {
+    await fs.access(DOCUMENTATION_DIR);
+    logger.debug('文档目录已存在');
+    
+    // 确保meta目录存在
+    try {
+      await fs.access(META_DIR);
+      logger.debug('文档meta目录已存在');
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        await fs.mkdir(META_DIR, { recursive: true });
+        logger.success('文档meta目录已创建');
+      } else {
+        throw error;
+      }
+    }
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      await fs.mkdir(DOCUMENTATION_DIR, { recursive: true });
+      logger.success('文档目录已创建');
+      
+      // 创建meta目录
+      await fs.mkdir(META_DIR, { recursive: true });
+      logger.success('文档meta目录已创建');
+    } else {
+      throw error;
+    }
+  }
+}
+
+// 获取文档列表
+async function getDocumentationList() {
+  try {
+    await ensureDocumentationDir();
+    const files = await fs.readdir(DOCUMENTATION_DIR);
+    
+    const documents = await Promise.all(files.map(async file => {
+      // 跳过目录和非文档文件
+      if (file === 'meta' || file.startsWith('.')) return null;
+      
+      // 处理JSON文件
+      if (file.endsWith('.json')) {
+        try {
+          const filePath = path.join(DOCUMENTATION_DIR, file);
+          const content = await fs.readFile(filePath, 'utf8');
+          const doc = JSON.parse(content);
+          return {
+            id: path.parse(file).name,
+            title: doc.title,
+            published: doc.published,
+            createdAt: doc.createdAt || new Date().toISOString(),
+            updatedAt: doc.updatedAt || new Date().toISOString()
+          };
+        } catch (fileError) {
+          logger.error(`读取JSON文档文件 ${file} 失败:`, fileError);
+          return null;
+        }
+      } 
+      
+      // 处理MD文件
+      if (file.endsWith('.md')) {
+        try {
+          const id = path.parse(file).name;
+          let metaData = { published: true, title: path.parse(file).name };
+          
+          // 尝试读取meta数据
+          try {
+            const metaPath = path.join(META_DIR, `${id}.json`);
+            const metaContent = await fs.readFile(metaPath, 'utf8');
+            metaData = { ...metaData, ...JSON.parse(metaContent) };
+          } catch (metaError) {
+            // meta文件不存在或无法解析,使用默认值
+            logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
+          }
+          
+          // 确保有发布状态
+          if (typeof metaData.published !== 'boolean') {
+            metaData.published = true;
+          }
+          
+          return {
+            id,
+            title: metaData.title || id,
+            path: file,  // 不直接加载内容,而是提供路径
+            published: metaData.published,
+            createdAt: metaData.createdAt || new Date().toISOString(),
+            updatedAt: metaData.updatedAt || new Date().toISOString()
+          };
+        } catch (mdError) {
+          logger.error(`处理MD文档 ${file} 失败:`, mdError);
+          return null;
+        }
+      }
+      
+      return null;
+    }));
+    
+    return documents.filter(doc => doc !== null);
+  } catch (error) {
+    logger.error('获取文档列表失败:', error);
+    throw error;
+  }
+}
+
+// 获取已发布文档
+async function getPublishedDocuments() {
+  const documents = await getDocumentationList();
+  return documents.filter(doc => doc.published);
+}
+
+// 获取单个文档
+async function getDocument(id) {
+  try {
+    await ensureDocumentationDir();
+    
+    // 首先尝试读取JSON文件
+    try {
+      const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
+      const jsonContent = await fs.readFile(jsonPath, 'utf8');
+      return JSON.parse(jsonContent);
+    } catch (jsonError) {
+      // JSON文件不存在,尝试读取MD文件
+      if (jsonError.code === 'ENOENT') {
+        const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
+        const mdContent = await fs.readFile(mdPath, 'utf8');
+        
+        // 读取meta数据
+        let metaData = { published: true, title: id };
+        try {
+          const metaPath = path.join(META_DIR, `${id}.json`);
+          const metaContent = await fs.readFile(metaPath, 'utf8');
+          metaData = { ...metaData, ...JSON.parse(metaContent) };
+        } catch (metaError) {
+          // meta文件不存在或无法解析,使用默认值
+          logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
+        }
+        
+        return {
+          id,
+          title: metaData.title || id,
+          content: mdContent,
+          published: metaData.published,
+          createdAt: metaData.createdAt || new Date().toISOString(),
+          updatedAt: metaData.updatedAt || new Date().toISOString()
+        };
+      }
+      
+      // 其他错误,直接抛出
+      throw jsonError;
+    }
+  } catch (error) {
+    logger.error(`获取文档 ${id} 失败:`, error);
+    throw error;
+  }
+}
+
+// 保存文档
+async function saveDocument(id, title, content) {
+  try {
+    await ensureDocumentationDir();
+    const docId = id || Date.now().toString();
+    const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
+    
+    // 检查是否已存在,保留发布状态
+    let published = false;
+    try {
+      const existingDoc = await fs.readFile(docPath, 'utf8');
+      published = JSON.parse(existingDoc).published || false;
+    } catch (error) {
+      // 文件不存在,使用默认值
+    }
+    
+    const now = new Date().toISOString();
+    const docData = {
+      title, 
+      content, 
+      published,
+      createdAt: now,
+      updatedAt: now
+    };
+    
+    await fs.writeFile(
+      docPath, 
+      JSON.stringify(docData, null, 2),
+      'utf8'
+    );
+    
+    return { id: docId, ...docData };
+  } catch (error) {
+    logger.error('保存文档失败:', error);
+    throw error;
+  }
+}
+
+// 删除文档
+async function deleteDocument(id) {
+  try {
+    await ensureDocumentationDir();
+    
+    // 删除JSON文件(如果存在)
+    try {
+      const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
+      await fs.unlink(jsonPath);
+    } catch (error) {
+      if (error.code !== 'ENOENT') {
+        logger.warn(`删除JSON文档 ${id} 失败:`, error);
+      }
+    }
+    
+    // 删除MD文件(如果存在)
+    try {
+      const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
+      await fs.unlink(mdPath);
+    } catch (error) {
+      if (error.code !== 'ENOENT') {
+        logger.warn(`删除MD文档 ${id} 失败:`, error);
+      }
+    }
+    
+    // 删除meta文件(如果存在)
+    try {
+      const metaPath = path.join(META_DIR, `${id}.json`);
+      await fs.unlink(metaPath);
+    } catch (error) {
+      if (error.code !== 'ENOENT') {
+        logger.warn(`删除文档 ${id} 的meta数据失败:`, error);
+      }
+    }
+    
+    return { success: true };
+  } catch (error) {
+    logger.error(`删除文档 ${id} 失败:`, error);
+    throw error;
+  }
+}
+
+// 切换文档发布状态
+async function toggleDocumentPublish(id) {
+  try {
+    await ensureDocumentationDir();
+    
+    // 尝试读取JSON文件
+    try {
+      const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
+      const content = await fs.readFile(jsonPath, 'utf8');
+      const doc = JSON.parse(content);
+      doc.published = !doc.published;
+      doc.updatedAt = new Date().toISOString();
+      
+      await fs.writeFile(jsonPath, JSON.stringify(doc, null, 2), 'utf8');
+      return doc;
+    } catch (jsonError) {
+      // 如果JSON文件不存在,尝试处理MD文件的meta数据
+      if (jsonError.code === 'ENOENT') {
+        const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
+        
+        // 确认MD文件存在
+        try {
+          await fs.access(mdPath);
+        } catch (mdError) {
+          throw new Error(`文档 ${id} 不存在`);
+        }
+        
+        // 获取或创建meta数据
+        const metaPath = path.join(META_DIR, `${id}.json`);
+        let metaData = { published: true, title: id };
+        
+        try {
+          const metaContent = await fs.readFile(metaPath, 'utf8');
+          metaData = { ...metaData, ...JSON.parse(metaContent) };
+        } catch (metaError) {
+          // meta文件不存在,使用默认值
+        }
+        
+        // 切换发布状态
+        metaData.published = !metaData.published;
+        metaData.updatedAt = new Date().toISOString();
+        
+        // 保存meta数据
+        await fs.writeFile(metaPath, JSON.stringify(metaData, null, 2), 'utf8');
+        
+        // 获取MD文件内容
+        const mdContent = await fs.readFile(mdPath, 'utf8');
+        
+        return {
+          id,
+          title: metaData.title,
+          content: mdContent,
+          published: metaData.published,
+          createdAt: metaData.createdAt,
+          updatedAt: metaData.updatedAt
+        };
+      }
+      
+      // 其他错误,直接抛出
+      throw jsonError;
+    }
+  } catch (error) {
+    logger.error(`切换文档 ${id} 发布状态失败:`, error);
+    throw error;
+  }
+}
+
+module.exports = {
+  ensureDocumentationDir,
+  getDocumentationList,
+  getPublishedDocuments,
+  getDocument,
+  saveDocument,
+  deleteDocument,
+  toggleDocumentPublish
+};

+ 331 - 0
hubcmdui/services/monitoringService.js

@@ -0,0 +1,331 @@
+/**
+ * 监控服务模块 - 处理容器状态监控和通知
+ */
+const axios = require('axios');
+const logger = require('../logger');
+const configService = require('./configService');
+const dockerService = require('./dockerService');
+
+// 监控相关状态映射
+let containerStates = new Map();
+let lastStopAlertTime = new Map();
+let secondAlertSent = new Set();
+let monitoringInterval = null;
+
+// 更新监控配置
+async function updateMonitoringConfig(config) {
+  try {
+    const currentConfig = await configService.getConfig();
+    currentConfig.monitoringConfig = {
+      ...currentConfig.monitoringConfig,
+      ...config
+    };
+    
+    await configService.saveConfig(currentConfig);
+    
+    // 重新启动监控
+    await startMonitoring();
+    
+    return { success: true };
+  } catch (error) {
+    logger.error('更新监控配置失败:', error);
+    throw error;
+  }
+}
+
+// 启动监控
+async function startMonitoring() {
+  try {
+    const config = await configService.getConfig();
+    const { isEnabled, monitorInterval } = config.monitoringConfig || {};
+    
+    // 如果监控已启用
+    if (isEnabled) {
+      const docker = await dockerService.getDockerConnection();
+      
+      if (docker) {
+        // 初始化容器状态
+        await initializeContainerStates(docker);
+        
+        // 如果已存在监控间隔,清除它
+        if (monitoringInterval) {
+          clearInterval(monitoringInterval);
+        }
+        
+        // 启动监控间隔
+        monitoringInterval = setInterval(async () => {
+          await checkContainerStates(docker, config.monitoringConfig);
+        }, (monitorInterval || 60) * 1000);
+        
+        // 监听Docker事件流
+        try {
+          const dockerEventStream = await docker.getEvents();
+          
+          dockerEventStream.on('data', async (chunk) => {
+            try {
+              const event = JSON.parse(chunk.toString());
+              
+              // 处理容器状态变化事件
+              if (event.Type === 'container' && 
+                 (event.Action === 'start' || event.Action === 'die' || 
+                  event.Action === 'stop' || event.Action === 'kill')) {
+                await handleContainerEvent(docker, event, config.monitoringConfig);
+              }
+            } catch (eventError) {
+              logger.error('处理Docker事件出错:', eventError);
+            }
+          });
+          
+          dockerEventStream.on('error', (err) => {
+            logger.error('Docker事件流错误:', err);
+          });
+        } catch (streamError) {
+          logger.error('无法获取Docker事件流:', streamError);
+        }
+        
+        return true;
+      }
+    } else if (monitoringInterval) {
+      // 如果监控已禁用但间隔仍在运行,停止它
+      clearInterval(monitoringInterval);
+      monitoringInterval = null;
+    }
+    
+    return false;
+  } catch (error) {
+    logger.error('启动监控失败:', error);
+    return false;
+  }
+}
+
+// 停止监控
+function stopMonitoring() {
+  if (monitoringInterval) {
+    clearInterval(monitoringInterval);
+    monitoringInterval = null;
+    logger.info('容器监控已停止');
+  }
+  return true;
+}
+
+// 初始化容器状态
+async function initializeContainerStates(docker) {
+  try {
+    const containers = await docker.listContainers({ all: true });
+    
+    for (const container of containers) {
+      const containerInfo = await docker.getContainer(container.Id).inspect();
+      containerStates.set(container.Id, containerInfo.State.Status);
+    }
+  } catch (error) {
+    logger.error('初始化容器状态失败:', error);
+  }
+}
+
+// 处理容器事件
+async function handleContainerEvent(docker, event, monitoringConfig) {
+  try {
+    const containerId = event.Actor.ID;
+    const container = docker.getContainer(containerId);
+    const containerInfo = await container.inspect();
+    
+    const newStatus = containerInfo.State.Status;
+    const oldStatus = containerStates.get(containerId);
+    
+    if (oldStatus && oldStatus !== newStatus) {
+      // 如果容器从停止状态变为运行状态
+      if (newStatus === 'running' && oldStatus !== 'running') {
+        await sendAlertWithRetry(
+          containerInfo.Name, 
+          `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, 
+          monitoringConfig
+        );
+        
+        // 清除告警状态
+        lastStopAlertTime.delete(containerInfo.Name);
+        secondAlertSent.delete(containerInfo.Name);
+      } 
+      // 如果容器从运行状态变为停止状态
+      else if (oldStatus === 'running' && newStatus !== 'running') {
+        await sendAlertWithRetry(
+          containerInfo.Name, 
+          `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, 
+          monitoringConfig
+        );
+        
+        // 记录停止时间,用于后续检查
+        lastStopAlertTime.set(containerInfo.Name, Date.now());
+        secondAlertSent.delete(containerInfo.Name);
+      }
+      
+      // 更新状态记录
+      containerStates.set(containerId, newStatus);
+    }
+  } catch (error) {
+    logger.error('处理容器事件失败:', error);
+  }
+}
+
+// 检查容器状态
+async function checkContainerStates(docker, monitoringConfig) {
+  try {
+    const containers = await docker.listContainers({ all: true });
+    
+    for (const container of containers) {
+      const containerInfo = await docker.getContainer(container.Id).inspect();
+      const newStatus = containerInfo.State.Status;
+      const oldStatus = containerStates.get(container.Id);
+      
+      // 如果状态发生变化
+      if (oldStatus && oldStatus !== newStatus) {
+        // 处理状态变化,与handleContainerEvent相同的逻辑
+        if (newStatus === 'running' && oldStatus !== 'running') {
+          await sendAlertWithRetry(
+            containerInfo.Name, 
+            `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, 
+            monitoringConfig
+          );
+          
+          lastStopAlertTime.delete(containerInfo.Name);
+          secondAlertSent.delete(containerInfo.Name);
+        } 
+        else if (oldStatus === 'running' && newStatus !== 'running') {
+          await sendAlertWithRetry(
+            containerInfo.Name, 
+            `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, 
+            monitoringConfig
+          );
+          
+          lastStopAlertTime.set(containerInfo.Name, Date.now());
+          secondAlertSent.delete(containerInfo.Name);
+        }
+        
+        containerStates.set(container.Id, newStatus);
+      } 
+      // 如果容器仍处于非运行状态,检查是否需要发送二次告警
+      else if (newStatus !== 'running') {
+        await checkSecondStopAlert(containerInfo.Name, newStatus, monitoringConfig);
+      }
+    }
+  } catch (error) {
+    logger.error('检查容器状态失败:', error);
+  }
+}
+
+// 检查是否需要发送二次停止告警
+async function checkSecondStopAlert(containerName, currentStatus, monitoringConfig) {
+  const now = Date.now();
+  const lastStopAlert = lastStopAlertTime.get(containerName) || 0;
+  
+  // 如果距离上次停止告警超过1小时,且还没有发送过第二次告警,则发送第二次告警
+  if (now - lastStopAlert >= 60 * 60 * 1000 && !secondAlertSent.has(containerName)) {
+    await sendAlertWithRetry(containerName, `仍未恢复 (当前状态: ${currentStatus})`, monitoringConfig);
+    secondAlertSent.add(containerName); // 标记已发送第二次告警
+  }
+}
+
+// 发送告警(带重试)
+async function sendAlertWithRetry(containerName, status, monitoringConfig, maxRetries = 6) {
+  const { notificationType, webhookUrl, telegramToken, telegramChatId } = monitoringConfig;
+  const cleanContainerName = containerName.replace(/^\//, '');
+  
+  for (let attempt = 1; attempt <= maxRetries; attempt++) {
+    try {
+      if (notificationType === 'wechat') {
+        await sendWechatAlert(webhookUrl, cleanContainerName, status);
+      } else if (notificationType === 'telegram') {
+        await sendTelegramAlert(telegramToken, telegramChatId, cleanContainerName, status);
+      }
+      
+      logger.success(`告警发送成功: ${cleanContainerName} ${status}`);
+      return;
+    } catch (error) {
+      if (attempt === maxRetries) {
+        logger.error(`达到最大重试次数,放弃发送告警: ${cleanContainerName} ${status}`);
+        logger.error('最后一次错误:', error);
+        return;
+      }
+      
+      logger.warn(`告警发送失败,尝试重试 (${attempt}/${maxRetries}): ${error.message}`);
+      await new Promise(resolve => setTimeout(resolve, 10000));
+    }
+  }
+}
+
+// 发送企业微信告警
+async function sendWechatAlert(webhookUrl, containerName, status) {
+  if (!webhookUrl) {
+    throw new Error('企业微信 Webhook URL 未设置');
+  }
+  
+  const response = await axios.post(webhookUrl, {
+    msgtype: 'text',
+    text: {
+      content: `Docker 容器告警: 容器 ${containerName} ${status}`
+    }
+  }, {
+    timeout: 5000
+  });
+  
+  if (response.status !== 200 || response.data.errcode !== 0) {
+    throw new Error(`请求成功但返回错误:${response.data.errmsg || JSON.stringify(response.data)}`);
+  }
+}
+
+// 发送Telegram告警
+async function sendTelegramAlert(token, chatId, containerName, status) {
+  if (!token || !chatId) {
+    throw new Error('Telegram Bot Token 或 Chat ID 未设置');
+  }
+  
+  const url = `https://api.telegram.org/bot${token}/sendMessage`;
+  const response = await axios.post(url, {
+    chat_id: chatId,
+    text: `Docker 容器告警: 容器 ${containerName} ${status}`
+  }, {
+    timeout: 5000
+  });
+  
+  if (response.status !== 200 || !response.data.ok) {
+    throw new Error(`发送Telegram消息失败:${JSON.stringify(response.data)}`);
+  }
+}
+
+// 测试通知
+async function testNotification(config) {
+  const { notificationType, webhookUrl, telegramToken, telegramChatId } = config;
+  
+  if (notificationType === 'wechat') {
+    await sendWechatAlert(webhookUrl, 'Test Container', 'This is a test notification');
+  } else if (notificationType === 'telegram') {
+    await sendTelegramAlert(telegramToken, telegramChatId, 'Test Container', 'This is a test notification');
+  } else {
+    throw new Error('不支持的通知类型');
+  }
+  
+  return { success: true };
+}
+
+// 切换监控状态
+async function toggleMonitoring(isEnabled) {
+  const config = await configService.getConfig();
+  config.monitoringConfig.isEnabled = isEnabled;
+  await configService.saveConfig(config);
+  
+  return startMonitoring();
+}
+
+// 获取已停止的容器
+async function getStoppedContainers(forceRefresh = false) {
+  return await dockerService.getStoppedContainers();
+}
+
+module.exports = {
+  updateMonitoringConfig,
+  startMonitoring,
+  stopMonitoring,
+  testNotification,
+  toggleMonitoring,
+  getStoppedContainers,
+  sendAlertWithRetry
+};

+ 52 - 0
hubcmdui/services/networkService.js

@@ -0,0 +1,52 @@
+/**
+ * 网络服务 - 提供网络诊断功能
+ */
+const { exec } = require('child_process');
+const { promisify } = require('util');
+const logger = require('../logger');
+
+const execAsync = promisify(exec);
+
+/**
+ * 执行网络测试
+ * @param {string} type 测试类型 ('ping' 或 'traceroute')
+ * @param {string} domain 目标域名
+ * @returns {Promise<string>} 测试结果
+ */
+async function performNetworkTest(type, domain) {
+  // 验证输入
+  if (!domain || !domain.match(/^[a-zA-Z0-9][a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)) {
+    throw new Error('无效的域名格式');
+  }
+  
+  if (!type || !['ping', 'traceroute'].includes(type)) {
+    throw new Error('无效的测试类型');
+  }
+  
+  try {
+    // 根据测试类型构建命令
+    const command = type === 'ping' 
+      ? `ping -c 4 ${domain}` 
+      : `traceroute -m 10 ${domain}`;
+      
+    logger.info(`执行网络测试: ${command}`);
+    
+    // 执行命令并获取结果
+    const { stdout, stderr } = await execAsync(command, { timeout: 30000 });
+    return stdout || stderr;
+  } catch (error) {
+    logger.error(`网络测试失败: ${error.message}`);
+    
+    // 如果命令被终止,表示超时
+    if (error.killed) {
+      throw new Error('测试超时');
+    }
+    
+    // 其他错误
+    throw error;
+  }
+}
+
+module.exports = {
+  performNetworkTest
+};

+ 103 - 0
hubcmdui/services/notificationService.js

@@ -0,0 +1,103 @@
+/**
+ * 通知服务
+ * 用于发送各种类型的通知
+ */
+const axios = require('axios');
+const logger = require('../logger');
+
+/**
+ * 发送通知
+ * @param {Object} message - 消息对象,包含标题、内容等
+ * @param {Object} config - 配置对象,包含通知类型和相关配置
+ * @returns {Promise<void>}
+ */
+async function sendNotification(message, config) {
+    const { type } = config;
+    
+    switch (type) {
+        case 'wechat':
+            return sendWechatNotification(message, config);
+        case 'telegram':
+            return sendTelegramNotification(message, config);
+        default:
+            throw new Error(`不支持的通知类型: ${type}`);
+    }
+}
+
+/**
+ * 发送企业微信通知
+ * @param {Object} message - 消息对象
+ * @param {Object} config - 配置对象
+ * @returns {Promise<void>}
+ */
+async function sendWechatNotification(message, config) {
+    const { webhookUrl } = config;
+    
+    if (!webhookUrl) {
+        throw new Error('企业微信 Webhook URL 未配置');
+    }
+    
+    const payload = {
+        msgtype: 'markdown',
+        markdown: {
+            content: `## ${message.title}\n${message.content}\n> ${message.time}`
+        }
+    };
+    
+    try {
+        const response = await axios.post(webhookUrl, payload, {
+            headers: { 'Content-Type': 'application/json' },
+            timeout: 5000
+        });
+        
+        if (response.status !== 200 || response.data.errcode !== 0) {
+            throw new Error(`企业微信返回错误: ${response.data.errmsg || '未知错误'}`);
+        }
+        
+        logger.info('企业微信通知发送成功');
+    } catch (error) {
+        logger.error('企业微信通知发送失败:', error);
+        throw new Error(`企业微信通知发送失败: ${error.message}`);
+    }
+}
+
+/**
+ * 发送Telegram通知
+ * @param {Object} message - 消息对象
+ * @param {Object} config - 配置对象
+ * @returns {Promise<void>}
+ */
+async function sendTelegramNotification(message, config) {
+    const { telegramToken, telegramChatId } = config;
+    
+    if (!telegramToken || !telegramChatId) {
+        throw new Error('Telegram Token 或 Chat ID 未配置');
+    }
+    
+    const text = `*${message.title}*\n\n${message.content}\n\n_${message.time}_`;
+    const url = `https://api.telegram.org/bot${telegramToken}/sendMessage`;
+    
+    try {
+        const response = await axios.post(url, {
+            chat_id: telegramChatId,
+            text: text,
+            parse_mode: 'Markdown'
+        }, {
+            headers: { 'Content-Type': 'application/json' },
+            timeout: 5000
+        });
+        
+        if (response.status !== 200 || !response.data.ok) {
+            throw new Error(`Telegram 返回错误: ${response.data.description || '未知错误'}`);
+        }
+        
+        logger.info('Telegram 通知发送成功');
+    } catch (error) {
+        logger.error('Telegram 通知发送失败:', error);
+        throw new Error(`Telegram 通知发送失败: ${error.message}`);
+    }
+}
+
+module.exports = {
+    sendNotification
+};

+ 55 - 0
hubcmdui/services/systemService.js

@@ -0,0 +1,55 @@
+/**
+ * 系统服务模块 - 处理系统级信息获取
+ */
+const { exec } = require('child_process');
+const os = require('os');
+const logger = require('../logger');
+
+// 获取磁盘空间信息
+async function getDiskSpace() {
+  try {
+    // 根据操作系统不同有不同的命令
+    const isWindows = os.platform() === 'win32';
+    
+    if (isWindows) {
+      // Windows实现(需要更复杂的逻辑)
+      return {
+        diskSpace: '未实现',
+        usagePercent: 0
+      };
+    } else {
+      // Linux/Mac实现
+      const diskInfo = await execPromise('df -h | grep -E "/$|/home" | head -1');
+      const diskParts = diskInfo.split(/\s+/);
+      
+      if (diskParts.length >= 5) {
+        return {
+          diskSpace: `${diskParts[2]}/${diskParts[1]}`,
+          usagePercent: parseInt(diskParts[4].replace('%', ''))
+        };
+      } else {
+        throw new Error('磁盘信息格式不正确');
+      }
+    }
+  } catch (error) {
+    logger.error('获取磁盘空间失败:', error);
+    throw error;
+  }
+}
+
+// 辅助函数: 执行命令
+function execPromise(command) {
+  return new Promise((resolve, reject) => {
+    exec(command, (error, stdout, stderr) => {
+      if (error) {
+        reject(error);
+        return;
+      }
+      resolve(stdout.trim());
+    });
+  });
+}
+
+module.exports = {
+  getDiskSpace
+};

+ 175 - 0
hubcmdui/services/userService.js

@@ -0,0 +1,175 @@
+/**
+ * 用户服务模块
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const bcrypt = require('bcrypt');
+const logger = require('../logger');
+
+const USERS_FILE = path.join(__dirname, '..', 'users.json');
+
+// 获取所有用户
+async function getUsers() {
+  try {
+    const data = await fs.readFile(USERS_FILE, 'utf8');
+    return JSON.parse(data);
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      logger.warn('Users file does not exist, creating default user');
+      const defaultUser = { 
+        username: 'root', 
+        password: bcrypt.hashSync('admin', 10),
+        createdAt: new Date().toISOString(),
+        loginCount: 0,
+        lastLogin: null
+      };
+      await saveUsers([defaultUser]);
+      return { users: [defaultUser] };
+    }
+    throw error;
+  }
+}
+
+// 保存用户
+async function saveUsers(users) {
+  await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8');
+}
+
+// 更新用户登录信息
+async function updateUserLoginInfo(username) {
+  try {
+    const { users } = await getUsers();
+    const user = users.find(u => u.username === username);
+    
+    if (user) {
+      user.loginCount = (user.loginCount || 0) + 1;
+      user.lastLogin = new Date().toISOString();
+      await saveUsers(users);
+    }
+  } catch (error) {
+    logger.error('更新用户登录信息失败:', error);
+  }
+}
+
+// 获取用户统计信息
+async function getUserStats(username) {
+  try {
+    const { users } = await getUsers();
+    const user = users.find(u => u.username === username);
+    
+    if (!user) {
+      return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
+    }
+    
+    // 计算账户年龄(如果有创建日期)
+    let accountAge = '0';
+    if (user.createdAt) {
+      const createdDate = new Date(user.createdAt);
+      const currentDate = new Date();
+      const diffTime = Math.abs(currentDate - createdDate);
+      const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+      accountAge = diffDays.toString();
+    }
+    
+    // 格式化最后登录时间
+    let lastLogin = '未知';
+    if (user.lastLogin) {
+      const lastLoginDate = new Date(user.lastLogin);
+      const now = new Date();
+      const isToday = lastLoginDate.toDateString() === now.toDateString();
+      
+      if (isToday) {
+        lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+      } else {
+        lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+      }
+    }
+    
+    return {
+      username: user.username,
+      loginCount: (user.loginCount || 0).toString(),
+      lastLogin,
+      accountAge
+    };
+  } catch (error) {
+    logger.error('获取用户统计信息失败:', error);
+    return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
+  }
+}
+
+// 创建新用户
+async function createUser(username, password) {
+  try {
+    const { users } = await getUsers();
+    
+    // 检查用户是否已存在
+    if (users.some(u => u.username === username)) {
+      throw new Error('用户名已存在');
+    }
+    
+    const hashedPassword = bcrypt.hashSync(password, 10);
+    const newUser = {
+      username,
+      password: hashedPassword,
+      createdAt: new Date().toISOString(),
+      loginCount: 0,
+      lastLogin: null
+    };
+    
+    users.push(newUser);
+    await saveUsers(users);
+    
+    return { success: true, username };
+  } catch (error) {
+    logger.error('创建用户失败:', error);
+    throw error;
+  }
+}
+
+// 修改用户密码
+async function changePassword(username, currentPassword, newPassword) {
+  try {
+    const { users } = await getUsers();
+    const user = users.find(u => u.username === username);
+    
+    if (!user) {
+      throw new Error('用户不存在');
+    }
+    
+    // 验证当前密码
+    const isMatch = await bcrypt.compare(currentPassword, user.password);
+    if (!isMatch) {
+      throw new Error('当前密码不正确');
+    }
+    
+    // 验证新密码复杂度(虽然前端做了,后端再做一层保险)
+    if (!isPasswordComplex(newPassword)) {
+         throw new Error('新密码不符合复杂度要求');
+    }
+    
+    // 更新密码
+    user.password = await bcrypt.hash(newPassword, 10);
+    await saveUsers(users);
+    
+    logger.info(`用户 ${username} 密码已成功修改`);
+  } catch (error) {
+    logger.error('修改密码失败:', error);
+    throw error;
+  }
+}
+
+// 验证密码复杂度 (从 userCenter.js 复制过来并调整)
+function isPasswordComplex(password) {
+    // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间
+    const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
+    return passwordRegex.test(password);
+}
+
+module.exports = {
+  getUsers,
+  saveUsers,
+  updateUserLoginInfo,
+  getUserStats,
+  createUser,
+  changePassword
+};

+ 58 - 0
hubcmdui/start-diagnostic.js

@@ -0,0 +1,58 @@
+/**
+ * 诊断启动脚本 - 运行诊断并安全启动服务器
+ */
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { runDiagnostics } = require('./scripts/diagnostics');
+
+// 确保必要的模块存在
+try {
+  require('./logger');
+} catch (error) {
+  console.error('无法加载logger模块,请确保该模块存在:', error.message);
+  process.exit(1);
+}
+
+const logger = require('./logger');
+
+async function startWithDiagnostics() {
+  logger.info('正在运行系统诊断...');
+  
+  try {
+    // 运行诊断
+    const { criticalErrors } = await runDiagnostics();
+    
+    if (criticalErrors.length > 0) {
+      logger.error('发现严重问题,无法启动系统。请修复问题后重试。');
+      process.exit(1);
+    }
+    
+    logger.success('诊断通过,正在启动系统...');
+    
+    // 启动服务器
+    const serverProcess = spawn('node', ['server.js'], {
+      stdio: 'inherit',
+      cwd: __dirname
+    });
+    
+    serverProcess.on('close', (code) => {
+      if (code !== 0) {
+        logger.error(`服务器进程异常退出,退出码: ${code}`);
+        process.exit(code);
+      }
+    });
+    
+    serverProcess.on('error', (err) => {
+      logger.error('启动服务器进程时出错:', err);
+      process.exit(1);
+    });
+    
+  } catch (error) {
+    logger.fatal('诊断过程中发生错误:', error);
+    process.exit(1);
+  }
+}
+
+// 启动服务
+startWithDiagnostics();

+ 3 - 1
hubcmdui/users.json

@@ -2,7 +2,9 @@
   "users": [
     {
       "username": "root",
-      "password": "$2b$10$tu.ceN0qpkl.RSR3fi/uy.9FfJGazUdWJCEPaJCDAhh6mPFbP0GxC"
+      "password": "$2b$10$lh1kqJtq3shL2BhMD1LbVOThGeAlPXsDgME/he4ZyDMRupVtj0Hl.",
+      "loginCount": 1,
+      "lastLogin": "2025-04-01T22:16:21.808Z"
     }
   ]
 }

BIN
hubcmdui/web/.DS_Store


+ 518 - 1882
hubcmdui/web/admin.html

@@ -7,10 +7,19 @@
     <link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
     <!-- 引入前端样式表 -->
     <link rel="stylesheet" href="style.css">
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css">
-    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
-    <script src="https://cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
+    <!-- 引入Bootstrap CSS和JS -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
+    <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
+    <!-- 引入 Markdown 编辑器 -->
+    <link rel="stylesheet" href="https://uicdn.toast.com/editor/3.0.0/toastui-editor.min.css" />
+    <script src="https://uicdn.toast.com/editor/3.0.0/toastui-editor.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+    
+    <!-- 自定义样式 -->
+    <link rel="stylesheet" href="css/admin.css">
     <style>
         /* 管理面板特定样式 */
         .admin-container {
@@ -28,6 +37,23 @@
             transition: all 0.3s ease;
         }
 
+        /* 文档管理新建文档徽章 */
+        .new-badge {
+            display: inline-block;
+            padding: 2px 8px;
+            background-color: #28a745;
+            color: white;
+            border-radius: 12px;
+            font-size: 0.75rem;
+            font-weight: bold;
+        }
+
+        @keyframes pulse {
+            0% { opacity: 1; }
+            50% { opacity: 0.7; }
+            100% { opacity: 1; }
+        }
+
         /* 用户信息部分样式 */
         .user-profile {
             padding: 1rem 1.5rem;
@@ -539,84 +565,163 @@
             font-size: 1rem;
         }
 
-        /* 表格样式 */
+        /* 表格样式 - 增强 Excel 效果 */
         .content-section table {
             width: 100%;
-            border-collapse: separate;
+            border-collapse: collapse; /* 合并边框 */
             border-spacing: 0;
             margin-top: 1.5rem;
             margin-bottom: 2rem;
-            border-radius: var (--radius-md);
-            overflow: hidden;
-            box-shadow: var(--shadow-sm);
+            border: 1px solid #ccc; /* 表格外边框 */
+            font-size: 0.9rem; /* 稍微减小字体 */
+            box-shadow: 0 2px 5px rgba(0,0,0,0.1); /* 轻微阴影 */
         }
 
         .content-section th {
-            background-color: var(--primary-color);
-            color: white;
-            font-weight: 500;
+            background-color: #f2f2f2; /* Excel 灰色表头背景 */
+            color: #333;
+            font-weight: bold; /* 加粗 */
             text-align: left;
-            padding: 1.2rem 1.5rem;
+            padding: 0.6rem 0.8rem; /* 调整内边距 */
+            border: 1px solid #ccc; /* 单元格边框 */
         }
 
         .content-section td {
-            padding: 1rem 1.5rem;
-            border-bottom: 1px solid var(--border-light);
+            padding: 0.6rem 0.8rem; /* 调整内边距 */
+            border: 1px solid #ddd; /* 单元格边框,比表头稍浅 */
             vertical-align: middle;
+            background-color: #ffffff;
+            color: #444; /* 单元格文字颜色 */
+            word-break: break-word; /* 防止长 ID 撑开单元格 */
         }
 
-        .content-section tr:last-child td {
-            border-bottom: none;
+        /* 可选:添加斑马纹效果 */
+        /*
+        .content-section tr:nth-child(even) td {
+            background-color: #f9f9f9;
         }
+        */
 
         .content-section tr:hover td {
-            background-color: rgba(61, 124, 244, 0.05);
+            background-color: #e9f5ff; /* 鼠标悬停高亮 */
         }
 
-        /* 操作按钮样式 */
-        .action-btn {
-            background-color: var(--primary-color);
-            color: white;
-            border: none;
-            padding: 0.6rem 0.9rem;
-            border-radius: var(--radius-sm);
-            cursor: pointer;
-            font-size: 0.9rem;
-            margin-right: 0.5rem;
-            transition: all var(--transition-fast);
-            display: inline-flex;
-            align-items: center;
-            gap: 0.25rem;
+        /* 表格头部悬停效果 */
+        .content-section th:hover {
+            background-color: #e8e8e8;
         }
 
-        .action-btn:hover {
-            background-color: var(--primary-dark);
-            transform: translateY(-2px);
-            box-shadow: var(--shadow-sm);
+        /* Excel风格的表格滚动条 */
+        .table-container {
+            overflow-x: auto;
+            max-width: 100%;
+            margin-bottom: 1.5rem;
+            border: 1px solid #e0e0e0;
+            border-radius: 4px;
         }
 
-        .action-btn:active {
-            transform: translateY(0);
+        /* 操作列样式 */
+        .action-cell {
+            text-align: center;
+            min-width: 120px;
         }
 
-        .edit-btn {
-            background-color: var(--primary-color);
+        /* 状态列样式 */
+        .status-running {
+            background-color: #28a745;
+            color: white;
+            padding: 0.3rem 0.6rem;
+            border-radius: 4px;
+            font-size: 0.8rem;
+            display: inline-block;
+            text-align: center;
         }
-
-        .edit-btn:before {
-            content: "\f044";
-            font-family: "Font Awesome 6 Free";
-            font-weight: 900;
+        
+        .status-stopped, .status-exited {
+            background-color: #dc3545;
+            color: white;
+            padding: 0.3rem 0.6rem;
+            border-radius: 4px;
+            font-size: 0.8rem;
+            display: inline-block;
+            text-align: center;
         }
-
-        .delete-btn {
-            background-color: var(--danger-color);
+        
+        .status-created {
+            background-color: #17a2b8;
+            color: white;
+            padding: 0.3rem 0.6rem;
+            border-radius: 4px;
+            font-size: 0.8rem;
+            display: inline-block;
+            text-align: center;
+        }
+        
+        .status-paused {
+            background-color: #ffc107;
+            color: #212529;
+            padding: 0.3rem 0.6rem;
+            border-radius: 4px;
+            font-size: 0.8rem;
+            display: inline-block;
+            text-align: center;
+        }
+        
+        .status-unknown {
+            background-color: #6c757d;
+            color: white;
+            padding: 0.3rem 0.6rem;
+            border-radius: 4px;
+            font-size: 0.8rem;
+            display: inline-block;
+            text-align: center;
         }
 
-        .delete-btn:before {
-            content: "\f2ed";
-            font-family: "Font Awesome 6 Free";
-            font-weight: 900;
+        /* 下拉菜单样式 */
+        .action-dropdown .dropdown-toggle {
+            border-color: #d0d0d0;
+            background-color: #fff;
+            color: #333;
+            font-size: 0.9rem;
+            padding: 0.4rem 0.8rem;
+        }
+        
+        .action-dropdown .dropdown-toggle:hover, 
+        .action-dropdown .dropdown-toggle:focus {
+            background-color: #f0f0f0;
+            box-shadow: none;
+        }
+        
+        .action-dropdown .dropdown-menu {
+            min-width: 150px;
+            padding: 0.3rem 0;
+            border: 1px solid #d0d0d0;
+            border-radius: 4px;
+            box-shadow: 0 3px 6px rgba(0,0,0,0.1);
+            font-size: 0.9rem;
+        }
+        
+        .action-dropdown .dropdown-item {
+            padding: 0.5rem 1rem;
+            color: #333;
+        }
+        
+        .action-dropdown .dropdown-item:hover {
+            background-color: #f0f7ff;
+        }
+        
+        .action-dropdown .dropdown-item i {
+            width: 1rem;
+            text-align: center;
+            margin-right: 0.5rem;
+        }
+        
+        .action-dropdown .text-danger {
+            color: #dc3545 !important;
+        }
+        
+        .action-dropdown .dropdown-divider {
+            margin: 0.3rem 0;
         }
 
         /* 登录模态框 */
@@ -629,7 +734,7 @@
             width: 100%;
             height: 100%;
             overflow: auto;
-            background: url('https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=1470&auto=format&fit=crop') center/cover no-repeat;
+            background: url('/images/login-bg.jpg') center/cover no-repeat;
             justify-content: center;
             align-items: center;
             animation: fadeIn 0.5s ease-in-out;
@@ -869,7 +974,7 @@
         }
 
         .config-form {
-            background-color: var(--container-bg);
+            background-color: var (--container-bg);
             padding: 1.8rem;
             border-radius: var(--radius-md);
             margin-bottom: 1.5rem;
@@ -990,7 +1095,7 @@
             color: var(--danger-color);
             margin-top: 0.75rem;
             padding: 0.75rem;
-            border-radius: var(--radius-md);
+            border-radius: var (--radius-md);;
             background-color: rgba(255, 82, 82, 0.1);
             font-size: 0.9rem;
             display: flex;
@@ -1005,10 +1110,10 @@
         }
 
         .success-message {
-            color: var(--success-color);
+            color: var (--success-color);
             margin-top: 0.75rem;
             padding: 0.75rem;
-            border-radius: var(--radius-md);
+            border-radius: var (--radius-md);
             background-color: rgba(50, 213, 131, 0.1);
             font-size: 0.9rem;
             display: flex;
@@ -1070,7 +1175,7 @@
             padding: 1rem;
             margin-bottom: 1rem;
             border: 1px solid var(--border-color);
-            border-radius: var(--radius-md);
+            border-radius: var (--radius-md);;
             font-size: 1.1rem;
             font-weight: 500;
             box-shadow: var(--shadow-sm);
@@ -1113,7 +1218,7 @@
             background-color: var(--success-color);
             color: white;
             padding: 0.8rem 1.2rem;
-            border-radius: var(--radius-md);
+            border-radius: var (--radius-md);
             display: inline-flex;
             align-items: center;
             gap: 0.5rem;
@@ -1123,9 +1228,8 @@
         }
 
         .add-btn:before {
-            content: "\f067";
-            font-family: "Font Awesome 6 Free";
-            font-weight: 900;
+            content: "";
+            /* 移除 Font Awesome 图标,以避免和 HTML 中的图标重复 */
         }
 
         .add-btn:hover {
@@ -1134,22 +1238,41 @@
             box-shadow: var(--shadow-md);
         }
 
-        /* Docker状态表格 */
-        .action-select {
-            padding: 0.5rem;
-            border-radius: var(--radius-sm);
-            border: 1px solid var(--border-color);
-            background-color: var(--container-bg);
-            color: var(--text-primary);
-            cursor: pointer;
-            width: 100%;
+        /* Docker容器操作按钮样式 */
+        .action-cell {
+            min-width: 200px;
         }
-
-        .action-select:focus {
-            border-color: var(--primary-color);
-            outline: none;
+        
+        .action-buttons {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 4px;
         }
-
+        
+        .action-buttons button {
+            padding: 3px 8px;
+            font-size: 0.8rem;
+            margin-right: 3px;
+            white-space: nowrap;
+        }
+        
+        .action-buttons button i {
+            margin-right: 3px;
+            font-size: 0.75rem;
+        }
+        
+        /* 改善按钮在小屏幕上的显示 */
+        @media (max-width: 768px) {
+            .action-buttons {
+                flex-direction: column;
+            }
+            
+            .action-buttons button {
+                margin-bottom: 3px;
+                width: 100%;
+            }
+        }
+        
         /* 响应式设计 */
         @media (max-width: 992px) {
             .admin-container {
@@ -1166,13 +1289,13 @@
                 flex-wrap: wrap;
                 justify-content: flex-start;
             }
-
+            
             .sidebar li {
                 padding: 0.5rem 1rem;
                 border-left: none;
                 border-bottom: 3px solid transparent;
             }
-
+            
             .sidebar li.active {
                 border-left: none;
                 border-bottom: 3px solid var(--primary-color);
@@ -1197,11 +1320,20 @@
             .button-group {
                 flex-direction: column;
             }
-
+            
             .btn, .action-btn {
                 width: 100%;
                 margin-bottom: 0.5rem;
             }
+            
+            .action-buttons {
+                flex-direction: column;
+            }
+            
+            .action-buttons button {
+                margin-bottom: 3px;
+                width: 100%;
+            }
         }
 
         @media (max-width: 480px) {
@@ -1210,6 +1342,182 @@
                 padding: 1.5rem;
             }
         }
+
+        /* Excel风格表格 */
+        .excel-table {
+            border-collapse: collapse;
+            width: 100%;
+            font-size: 14px;
+            border: 1px solid #ccc;
+        }
+        .excel-table th {
+            background-color: #f2f2f2;
+            border: 1px solid #ccc;
+            padding: 8px;
+            text-align: center;
+            position: sticky;
+            top: 0;
+            z-index: 10;
+        }
+        .excel-table td {
+            border: 1px solid #ccc;
+            padding: 6px 8px;
+            text-align: left;
+        }
+        .excel-table tr:nth-child(even) {
+            background-color: #f9f9f9;
+        }
+        .excel-table tr:hover {
+            background-color: #f0f7ff;
+        }
+        
+        /* 容器更新样式 */
+        .update-progress {
+            padding: 1rem;
+            text-align: center;
+        }
+        
+        .update-progress p {
+            margin-bottom: 1rem;
+            font-size: 1rem;
+        }
+        
+        .update-progress strong {
+            color: #007bff;
+            font-weight: 600;
+        }
+        
+        .progress-status {
+            margin: 1rem 0;
+            font-size: 0.9rem;
+            color: #555;
+            font-weight: 500;
+        }
+        
+        .progress-container {
+            width: 100%;
+            height: 10px;
+            background-color: #f0f0f0;
+            border-radius: 5px;
+            overflow: hidden;
+            margin: 1rem 0;
+        }
+        
+        .progress-bar {
+            height: 100%;
+            background-color: #4CAF50;
+            width: 0%;
+            transition: width 0.3s ease-in-out;
+            border-radius: 5px;
+            position: relative;
+        }
+        
+        /* 修改SweetAlert样式 */
+        .swal2-popup.update-popup {
+            border-radius: 10px;
+            padding: 1.5rem;
+            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+        }
+        
+        .swal2-title.update-title {
+            font-size: 1.5rem;
+            font-weight: 600;
+            color: #333;
+        }
+        
+        .swal2-input.update-input {
+            box-shadow: none;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+            padding: 0.75rem;
+            font-size: 0.95rem;
+        }
+        
+        .swal2-input.update-input:focus {
+            border-color: #3085d6;
+            box-shadow: 0 0 0 3px rgba(48, 133, 214, 0.2);
+        }
+        
+        /* 容器日志样式 */
+        .container-logs {
+            max-height: 70vh;
+            overflow-y: auto;
+            background: #1e1e1e;
+            color: #f0f0f0;
+            padding: 1rem;
+            border-radius: 5px;
+            font-family: 'Courier New', monospace;
+            font-size: 0.9rem;
+            white-space: pre-wrap;
+            word-break: break-all;
+        }
+        
+        .swal2-logs-container {
+            max-width: 100%;
+            padding: 0 !important;
+        }
+        
+        .swal2-logs-popup {
+            max-width: 80% !important;
+        }
+        
+        /* 容器状态标签样式 */
+        .badge.status-running {
+            background-color: #28a745;
+            color: white;
+            padding: 5px 8px;
+            border-radius: 4px;
+            display: inline-block;
+            min-width: 70px;
+            text-align: center;
+        }
+        
+        .badge.status-stopped, .badge.status-exited {
+            background-color: #dc3545;
+            color: white;
+            padding: 5px 8px;
+            border-radius: 4px;
+            display: inline-block;
+            min-width: 70px;
+            text-align: center;
+        }
+        
+        .badge.status-created {
+            background-color: #17a2b8;
+            color: white;
+            padding: 5px 8px;
+            border-radius: 4px;
+            display: inline-block;
+            min-width: 70px;
+            text-align: center;
+        }
+        
+        .badge.status-unknown, .badge.status-paused {
+            background-color: #6c757d;
+            color: white;
+            padding: 5px 8px;
+            border-radius: 4px;
+            display: inline-block;
+            min-width: 70px;
+            text-align: center;
+        }
+        
+        /* 容器表格标题样式 */
+        .docker-table-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 15px;
+            padding-bottom: 10px;
+            border-bottom: 1px solid #eee;
+        }
+        
+        .docker-table-title {
+            font-size: 18px;
+            font-weight: 600;
+            color: #333;
+            margin: 0;
+        }
     </style>
 </head>
 <body>
@@ -1233,7 +1541,7 @@
                 </div>
             </div>
             <h2><i class="fas fa-cogs"></i>管理面板</h2>
-            <ul>
+            <ul class="sidebar-nav">
                 <li data-section="dashboard" class="active">
                     <i class="fas fa-tachometer-alt"></i>控制面板
                 </li>
@@ -1268,7 +1576,7 @@
                         <div class="welcome-content">
                             <h1 class="welcome-title">欢迎回来,<span id="welcomeUsername">管理员</span>!</h1>
                             <p class="welcome-subtitle">这里是 Docker 镜像代理加速系统控制面板</p>
-                            <button class="welcome-action" onclick="refreshSystemStatus()">
+                            <button class="welcome-action" id="refreshSystemBtn">
                                 <i class="fas fa-sync-alt"></i> 刷新系统状态
                             </button>
                         </div>
@@ -1280,142 +1588,78 @@
                     </div>
 
                     <div class="dashboard-grid">
-                        <div class="dashboard-card">
-                            <div class="card-icon">
-                                <i class="fab fa-docker"></i>
-                            </div>
-                            <div class="card-title">容器总数</div>
-                            <div class="card-value" id="totalContainers">--</div>
-                            <div class="card-description">当前系统中的容器总数</div>
-                            <div class="card-footer">
-                                <div class="trend up">
-                                    <i class="fas fa-arrow-up"></i>
-                                    <span>正常运行</span>
-                                </div>
-                                <div class="card-action" onclick="showSection('docker-status')">查看详情</div>
-                            </div>
-                        </div>
-
-                        <div class="dashboard-card">
-                            <div class="card-icon" style="background-color: rgba(255, 152, 0, 0.1); color: #FF9800;">
-                                <i class="fas fa-memory"></i>
-                            </div>
-                            <div class="card-title">系统内存</div>
-                            <div class="card-value" id="systemMemory">--</div>
-                            <div class="card-description">当前系统内存使用情况</div>
-                            <div class="card-footer">
-                                <div class="trend up">
-                                    <i class="fas fa-arrow-up"></i>
-                                    <span>运行正常</span>
-                                </div>
-                                <div class="card-action" onclick="showSection('docker-monitoring')">监控详情</div>
-                            </div>
-                        </div>
-
-                        <div class="dashboard-card">
-                            <div class="card-icon" style="background-color: rgba(76, 175, 80, 0.1); color: #4CAF50;">
-                                <i class="fas fa-microchip"></i>
-                            </div>
-                            <div class="card-title">CPU 负载</div>
-                            <div class="card-value" id="cpuLoad">--</div>
-                            <div class="card-description">当前系统 CPU 负载情况</div>
-                            <div class="card-footer">
-                                <div class="trend down">
-                                    <i class="fas fa-arrow-down"></i>
-                                    <span>降低 5%</span>
-                                </div>
-                                <div class="card-action" onclick="showSection('docker-monitoring')">监控详情</div>
-                            </div>
-                        </div>
-
-                        <div class="dashboard-card">
-                            <div class="card-icon" style="background-color: rgba(33, 150, 243, 0.1); color: #2196F3;">
-                                <i class="fas fa-hdd"></i>
-                            </div>
-                            <div class="card-title">磁盘空间</div>
-                            <div class="card-value" id="diskSpace">--</div>
-                            <div class="card-description">当前系统磁盘使用情况</div>
-                            <div class="card-footer">
-                                <div class="trend up">
-                                    <i class="fas fa-arrow-up"></i>
-                                    <span>充足</span>
-                                </div>
-                                <div class="card-action" onclick="checkDiskSpace()">查看详情</div>
-                            </div>
-                        </div>
+                        <!-- 仪表板卡片将由 systemStatus.initDashboard() 动态生成 -->
                     </div>
 
                     <div class="dashboard-card">
                         <h3 class="card-title">最近容器操作</h3>
                         <table id="recentActivitiesTable">
-                            <thead>
-                                <tr>
-                                    <th>时间</th>
-                                    <th>操作</th>
-                                    <th>容器</th>
-                                    <th>状态</th>
-                                </tr>
-                            </thead>
-                            <tbody id="recentActivitiesBody">
-                                <tr>
-                                    <td colspan="4" style="text-align: center;">加载中...</td>
-                                </tr>
-                            </tbody>
+                            <!-- 活动表内容将由 systemStatus.refreshSystemStatus() 动态更新 -->
                         </table>
                     </div>
                 </div>
 
-                <!-- 将原有的内容分成四个部分,每个部分对应一个侧边栏项目 -->
+                <!-- 基本配置 -->
                 <div id="basic-config" class="content-section">
-                    <label for="logoUrl">Logo URL: (可选)</label>
-                    <input type="url" id="logoUrl" name="logoUrl">
-                    <button type="button" onclick="saveLogo()">保存 Logo</button>
-
-                    <label for="proxyDomain">Docker镜像代理地址: (必填)</label>
-                    <input type="text" id="proxyDomain" name="proxyDomain" required>
-                    <button type="button" onclick="saveProxyDomain()">保存代理地址</button>
+                    <h1 class="admin-title">基本配置</h1>
+                    <div class="config-form">
+                        <div class="form-group">
+                            <label for="logoUrl">Logo URL: (可选)</label>
+                            <input type="url" id="logoUrl" name="logoUrl" class="form-control">
+                        </div>
+                        <button type="button" class="btn btn-primary" onclick="saveConfig({logo: document.getElementById('logoUrl').value})">保存 Logo</button>
+                    
+                        <div class="form-group" style="margin-top: 2rem;">
+                            <label for="proxyDomain">Docker镜像代理地址: (必填)</label>
+                            <input type="text" id="proxyDomain" name="proxyDomain" class="form-control" required>
+                        </div>
+                        <button type="button" class="btn btn-primary" onclick="saveConfig({proxyDomain: document.getElementById('proxyDomain').value})">保存代理地址</button>
+                    </div>
                 </div>
                 
+                <!-- 菜单管理 -->
                 <div id="menu-management" class="content-section">
                     <h2 class="menu-label">菜单项管理</h2>
                     <table id="menuTable">
                         <thead>
-                            <tr>
-                                <th>文本</th>
-                                <th>链接 (可选)</th>
-                                <th>新标签页打开</th>
-                                <th>操作</th>
-                            </tr>
+                            <!-- 表头将由 menuManager.renderMenuItems() 动态生成 -->
                         </thead>
                         <tbody id="menuTableBody">
                             <!-- 菜单项将在这里动态添加 -->
                         </tbody>
                     </table>
-                    <button type="button" class="add-btn" onclick="showNewMenuItemRow()">添加菜单项</button>
+                    <button type="button" class="add-btn" onclick="menuManager.showNewMenuItemRow()">
+                        <i class="fas fa-plus"></i> 添加菜单项
+                    </button>
                 </div>
   
                 <!-- 文档管理部分 -->
                 <div id="documentation-management" class="content-section">
                     <h2 class="menu-label">文档管理</h2>
-                    <button type="button" onclick="newDocument()">新建文档</button>
+                    <div class="action-bar" style="margin-bottom: 15px;">
+                        <button type="button" class="btn btn-primary" onclick="documentManager.newDocument()">
+                            <i class="fas fa-plus"></i> 新建文档
+                        </button>
+                    </div>
+                    
                     <table id="documentTable">
                         <thead>
-                            <tr>
-                                <th>文章</th>
-                                <th>操作</th>
-                            </tr>
+                            <!-- 表头将由 documentManager.renderDocumentList() 动态生成 -->
                         </thead>
                         <tbody id="documentTableBody">
                             <!-- 文档列表将在这里动态添加 -->
                         </tbody>
                     </table>
+                    
                     <div id="editorContainer" style="display: none;">
-                        <input type="text" id="documentTitle" placeholder="文档标题">
-                        <div id="editormd">
-                            <textarea style="display:none;"></textarea>
+                        <input type="text" id="documentTitle" placeholder="请输入文档标题" autocomplete="off">
+                        <div id="editor">
+                            <!-- 编辑器将在这里初始化 -->
+                        </div>
+                        <div class="editor-actions">
+                            <button type="button" class="btn btn-secondary" onclick="documentManager.cancelEdit()">取消</button>
+                            <button type="button" class="btn btn-primary" onclick="documentManager.saveDocument()">保存文档</button>
                         </div>
-                        <button type="button" onclick="saveDocument()">保存文档</button>
-                        <button type="button" onclick="cancelEdit()">取消</button>
                     </div>
                 </div>  
 
@@ -1426,9 +1670,9 @@
                     <input type="password" id="currentPassword" name="currentPassword">
                     <label for="newPassword">新密码</label>
                     <span class="password-hint" id="passwordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
-                    <input type="password" id="newPassword" name="newPassword" oninput="checkPasswordStrength()">
+                    <input type="password" id="newPassword" name="newPassword" oninput="userCenter.checkPasswordStrength()">
                     <span id="passwordStrength" style="color: red;"></span>
-                    <button type="button" onclick="changePassword()">修改密码</button>
+                    <button type="button" onclick="userCenter.changePassword()">修改密码</button>
                 </div>
 
                 <!-- 网络测试 -->
@@ -1437,50 +1681,38 @@
                     <div class="input-group">
                         <label for="domainSelect">目标域名:</label>
                         <select id="domainSelect" class="form-control">
-                            <option value="">选择预定义域名</option>
-                            <option value="gcr.io">gcr.io</option>
-                            <option value="ghcr.io">ghcr.io</option>
-                            <option value="quay.io">quay.io</option>
-                            <option value="k8s.gcr.io">k8s.gcr.io</option>
-                            <option value="registry.k8s.io">registry.k8s.io</option>
-                            <option value="mcr.microsoft.com">mcr.microsoft.com</option>
-                            <option value="docker.elastic.co">docker.elastic.co</option>
-                            <option value="registry-1.docker.io">registry-1.docker.io</option>
+                            <!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
                         </select>
                     </div>
                     <div class="input-group">
                         <label for="testType">测试类型:</label>
                         <select id="testType">
-                            <option value="ping">Ping</option>
-                            <option value="traceroute">Traceroute</option>
+                            <!-- 选项将由 networkTest.initNetworkTest() 动态生成 -->
                         </select>
                     </div>
-                    <button>开始测试</button>
+                    <button class="btn btn-primary">开始测试</button>
                     <div id="testResults" style="margin-top: 20px; white-space: pre-wrap; font-family: monospace;"></div>
                 </div>
 
                 <!-- Docker服务状态 -->
                 <div id="docker-status" class="content-section">
-                    <h1 class="admin-title">Docker 服务状态</h1>
-                    <table id="dockerStatusTable">
-                      <thead>
-                        <tr>
-                          <th>容器 ID</th>
-                          <th>名称</th>
-                          <th>镜像</th>
-                          <th>状态</th>
-                          <th>CPU</th>
-                          <th>内存</th>
-                          <th>创建时间</th>
-                        </tr>
-                      </thead>
-                      <tbody id="dockerStatusTableBody">
-                        <!-- Docker 容器状态将在这里动态添加 -->
-                      </tbody>
-                    </table>
-                    <button type="button" onclick="refreshDockerStatus()">刷新状态</button>
-                </div>
-
+                    <div class="section-heading">
+                        <h2 class="section-title">Docker 服务状态</h2>
+                        <div class="section-description">
+                            查看和管理Docker容器状态和操作
+                        </div>
+                    </div>
+                    
+                    <!-- 新增表格容器以支持标题和操作区域 -->
+                    <div id="dockerTableContainer" class="table-responsive">
+                        <table id="dockerStatusTable" class="excel-table">
+                            <thead></thead>
+                            <tbody id="dockerStatusTableBody"></tbody>
+                        </table>
+                    </div>
+                </div>
+
+                <!-- Docker监控配置 -->
                 <div id="docker-monitoring" class="content-section">
                     <h1 class="admin-title">Docker 容器监控</h1>
                     <div class="monitoring-status">
@@ -1490,7 +1722,7 @@
                     <div class="config-form">
                         <div class="form-group">
                             <label for="notificationType">通知方式:</label>
-                            <select id="notificationType" class="form-control" onchange="toggleNotificationFields()">
+                            <select id="notificationType" class="form-control">
                                 <option value="wechat">企业微信群机器人</option>
                                 <option value="telegram">Telegram Bot</option>
                             </select>
@@ -1516,9 +1748,9 @@
                             <input type="number" id="monitorInterval" name="monitorInterval" min="1" value="60" class="form-control">
                         </div>
                         <div class="button-group">
-                            <button onclick="testNotification()" class="btn btn-secondary">测试通知</button>
-                            <button onclick="saveMonitoringConfig()" class="btn btn-primary">保存配置</button>
-                            <button onclick="toggleMonitoring()" class="btn btn-secondary" id="toggleMonitoringBtn">开启/关闭监控</button>
+                            <button class="btn btn-secondary" id="testNotifyBtn" onclick="app.testNotification()">测试通知</button>
+                            <button class="btn btn-primary" id="saveConfigBtn" onclick="app.saveMonitoringConfig()">保存配置</button>
+                            <button class="btn btn-secondary" id="toggleMonitoringBtn" onclick="app.toggleMonitoring()">开启/关闭监控</button>
                         </div>
                     </div>
                     <div id="messageContainer"></div>
@@ -1537,14 +1769,13 @@
                     </div>
                 </div>
 
-                <!-- 用户中心部分 - 修改按钮样式,移除图标 -->
+                <!-- 用户中心部分 -->
                 <div id="user-center" class="content-section">
                     <div class="user-center-header">
                         <div>
                             <h1 class="user-center-title">用户中心</h1>
                             <p class="user-center-subtitle">管理您的个人信息和账户安全</p>
                         </div>
-                        <!-- Fix 4: Remove icon from user center buttons -->
                         <button class="btn btn-primary" id="ucLogoutBtn" style="display: inline-block;">退出登录</button>
                     </div>
                     
@@ -1565,30 +1796,23 @@
                                     <div class="stat-label">账户天数</div>
                                 </div>
                             </div>
+                            
+                            <!-- 移除用户详细信息部分 -->
                         </div>
                         
                         <div class="user-center-section">
                             <h2 class="user-center-section-title">修改密码</h2>
-                            <label for="currentPassword">当前密码</label>
-                            <input type="password" id="currentPassword" name="currentPassword">
-                            <label for="newPassword">新密码</label>
-                            <span class="password-hint" id="passwordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
-                            <input type="password" id="newPassword" name="newPassword" oninput="checkPasswordStrength()">
-                            <span id="passwordStrength" style="color: red;"></span>
-                            <button type="button" onclick="changePassword()">修改密码</button>
-                        </div>
-                        
-                        <div class="user-center-section">
-                            <h2 class="user-center-section-title">安全设置</h2>
-                            <div class="form-group">
-                                <label for="loginNotification">登录通知</label>
-                                <select id="loginNotification" class="form-control">
-                                    <option value="none">不通知</option>
-                                    <option value="email">邮件通知</option>
-                                </select>
-                            </div>
-                            <!-- Fix 4: Remove icon from user center save button -->
-                            <button type="button" class="btn btn-primary" id="saveSecurityBtn" style="display: inline-block;">保存设置</button>
+                            <form id="changePasswordForm">
+                                <label for="ucCurrentPassword">当前密码</label>
+                                <input type="password" id="ucCurrentPassword" name="currentPassword">
+                                <label for="ucNewPassword">新密码</label>
+                                <span class="password-hint" id="ucPasswordHint">密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间</span>
+                                <input type="password" id="ucNewPassword" name="newPassword" oninput="userCenter.checkUcPasswordStrength()">
+                                <label for="ucConfirmPassword">确认新密码</label>
+                                <input type="password" id="ucConfirmPassword" name="confirmPassword">
+                                <span id="ucPasswordStrength" style="color: red;"></span>
+                                <button type="submit" class="btn btn-primary">修改密码</button>
+                            </form>
                         </div>
                     </div>
                 </div>
@@ -1602,1672 +1826,84 @@
             <div class="login-header">
                 <h2>管理员登录</h2>
             </div>
-            <form class="login-form">
+    
+            <form id="loginForm" class="login-form">
                 <input type="text" id="username" name="username" placeholder="用户名" required>
                 <input type="password" id="password" name="password" placeholder="密码" required>
+                <!-- 修复这里的结构问题 - 添加了缺失的开始标签 -->
                 <div class="captcha-container">
                     <input type="text" id="captcha" name="captcha" placeholder="验证码" required>
-                    <span id="captchaText" onclick="refreshCaptcha()">点击刷新验证码</span>
+                    <span id="captchaText" onclick="auth.refreshCaptcha()">点击刷新验证码</span>
                 </div>
-                <button type="button" onclick="login()">登录</button>
+                <button type="submit" id="loginButton">登录</button>
             </form>
         </div>
     </div>
 
     <script src="https://cdnjs.cloudflare.com/ajax/libs/dragula/3.7.2/dragula.min.js"></script>
     <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
-    <script>
-        function persistSession() {
-            if (document.cookie.includes('connect.sid')) {
-                sessionStorage.setItem('sessionActive', 'true');
-            }
-        }
-
-        function checkSessionPersistence() {
-            return sessionStorage.getItem('sessionActive') === 'true';
-        }
-        
-        let menuItems = [];
-        let isLoggedIn = false;
-        let editingIndex = -1;
-        let editor;
-        let currentEditingDoc = null;
-        let documents = [];
-        
-
-        function showSection(sectionId) {
-            console.log('Showing section:', sectionId);
-            
-            // 获取所有内容部分和侧边栏项目
-            const contentSections = document.querySelectorAll('.content-section');
-            const sidebarItems = document.querySelectorAll('.sidebar li');
-            
-            // 确保目标部分存在
-            const targetSection = document.getElementById(sectionId);
-            if (!targetSection) {
-                console.error('Section not found:', sectionId);
-                return;
-            }
-            
-            contentSections.forEach(section => {
-                if (section.id === sectionId) {
-                    section.classList.add('active');
-                    
-                    // 加载特定部分的内容
-                    if (sectionId === 'docker-status') {
-                        loadDockerStatus();
-                    } else if (sectionId === 'user-center') {
-                        loadUserStats();
-                    } else if (sectionId === 'docker-monitoring') {
-                        loadMonitoringConfig();
-                        refreshStoppedContainers();
-                    }
-                } else {
-                    section.classList.remove('active');
-                }
-            });
-            
-            // 保存当前部分以便持久化
-            localStorage.setItem('currentSection', sectionId);
-
-            // 更新侧边栏活动状态
-            sidebarItems.forEach(item => {
-                if (item.getAttribute('data-section') === sectionId) {
-                    item.classList.add('active');
-                } else {
-                    item.classList.remove('active');
-                }
-            });
-        }
-
-
-        function bindUserActionButtons() {
-            // 确保这些按钮有事件监听器
-            const userCenterBtn = document.getElementById('userCenterBtn');
-            if (userCenterBtn) {
-                userCenterBtn.addEventListener('click', function() {
-                    showSection('user-center');
-                });
-            }
-            
-            const logoutBtn = document.getElementById('logoutBtn');
-            if (logoutBtn) {
-                logoutBtn.addEventListener('click', logout);
-            }
-            
-            const ucLogoutBtn = document.getElementById('ucLogoutBtn');
-            if (ucLogoutBtn) {
-                ucLogoutBtn.addEventListener('click', logout);
-            }
-        }
-        
-        function bindDashboardCardActions() {
-            // 为每个仪表板卡片添加明确的事件处理程序
-            const dashboardCards = document.querySelectorAll('.dashboard-card');
-            
-            dashboardCards.forEach(card => {
-                const actionBtn = card.querySelector('.card-action');
-                const cardTitle = card.querySelector('.card-title').textContent.trim();
-                
-                if (actionBtn) {
-                    actionBtn.addEventListener('click', function() {
-                        console.log('卡片动作点击:', cardTitle);
-                        
-                        if (cardTitle === '容器总数') {
-                            showSection('docker-status');
-                        } else if (cardTitle === '系统内存' || cardTitle === 'CPU 负载') {
-                            showSection('docker-monitoring');
-                        } else if (cardTitle === '磁盘空间') {
-                            checkDiskSpace();
-                        }
-                    });
-                }
-            });
-            
-            // 为刷新系统状态添加点击处理程序
-            const refreshBtn = document.querySelector('.welcome-action');
-            if (refreshBtn) {
-                refreshBtn.addEventListener('click', refreshSystemStatus);
-            }
-        }
-
-        // 初始化编辑器
-        function initEditor() {
-            if (editor) {
-                console.log('Editor already initialized');
-                return;
-            }
-            try {
-                editor = editormd("editormd", {
-                    width: "100%",
-                    height: 640,
-                    path : "https://cdn.jsdelivr.net/npm/[email protected]/lib/",
-                    theme : "default",
-                    previewTheme : "default",
-                    editorTheme : "default",
-                    markdown : "",
-                    codeFold : true,
-                    saveHTMLToTextarea : true,
-                    searchReplace : true,
-                    watch : true, // 开启实时预览
-                    htmlDecode : "style,script,iframe|on*",
-                    toolbar : true,
-                    toolbarIcons : "full",
-                    placeholder: "请输入Markdown格式的文档...",
-                    emoji : true,
-                    taskList : true,
-                    tocm : true,
-                    tex : true,
-                    flowChart : true,
-                    sequenceDiagram : true,
-                    onload : function() {
-                        console.log('Editor.md loaded successfully');
-                        // 在加载完成后,立即切换到编辑模式
-                        this.unwatch();
-                        this.watch();
-                    }
-                });
-                console.log('Editor initialized successfully');
-            } catch (error) {
-                console.error('Error initializing editor:', error);
-            }
-        }
-
-
-        function newDocument() {
-            currentEditingDoc = null;
-            document.getElementById('documentTitle').value = '';
-            editor.setMarkdown('');
-            showEditor();
-        }
-
-        function showEditor() {
-            document.getElementById('documentTable').style.display = 'none';
-            document.getElementById('editorContainer').style.display = 'block';
-            if (editor) {
-                // 确保每次显示编辑器时都切换到编辑模式
-                editor.unwatch();
-                editor.watch();
-            }
-        }
-
-        function hideEditor() {
-            document.getElementById('documentTable').style.display = 'table';
-            document.getElementById('editorContainer').style.display = 'none';
-        }
-
-        function cancelEdit() {
-            hideEditor();
-        }
-
-        async function saveDocument() {
-            const title = document.getElementById('documentTitle').value.trim();
-            const content = editor.getMarkdown();
-            if (!title) {
-                showAlert('请输入文档标题', 'error');
-                return;
-            }
-            try {
-                // 显示保存中状态
-                Swal.fire({
-                    title: '正在保存',
-                    text: '请稍候...',
-                    allowOutsideClick: false,
-                    didOpen: () => {
-                        Swal.showLoading();
-                    }
-                });
-                
-                const response = await fetch('/api/documentation', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ 
-                        id: currentEditingDoc ? currentEditingDoc.id : null, 
-                        title, 
-                        content 
-                    })
-                });
-                
-                if (response.ok) {
-                    Swal.fire({
-                        title: '保存成功',
-                        icon: 'success',
-                        timer: 1500,
-                        showConfirmButton: false
-                    }).then(() => {
-                        hideEditor();
-                        loadDocumentList();
-                    });
-                } else {
-                    throw new Error('保存失败');
-                }
-            } catch (error) {
-                console.error('保存文档失败:', error);
-                showAlert('保存文档失败: ' + error.message, 'error');
-            }
-        }
-
-        async function loadDocumentList() {
-            try {
-                const response = await fetch('/api/documentation-list');
-                if (!response.ok) {
-                    const errorData = await response.json();
-                    throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error}, details: ${errorData.details}`);
-                }
-                documents = await response.json();
-                console.log('Received documents:', documents);
-                renderDocumentList();
-            } catch (error) {
-                console.error('加载文档列表失败:', error);
-                showAlert('加载文档列表失败: ' + error.message, 'error');
-            }
-        }
-
-        function renderDocumentList() {
-            const tbody = document.getElementById('documentTableBody');
-            tbody.innerHTML = '';
-            if (documents.length === 0) {
-                tbody.innerHTML = '<tr><td colspan="2">没有找到文档</td></tr>';
-                return;
-            }
-            documents.forEach(doc => {
-                const row = `
-                    <tr>
-                        <td>${doc.title || 'Untitled Document'}</td>
-                        <td>
-                            <button onclick="editDocument('${doc.id}')">编辑</button>
-                            <button onclick="deleteDocument('${doc.id}')">删除</button>
-                            <button onclick="togglePublish('${doc.id}')">${doc.published ? '取消发布' : '发布'}</button>
-                        </td>
-                    </tr>
-                `;
-                tbody.innerHTML += row;
-            });
-        }
-
-        function editDocument(id) {
-            currentEditingDoc = documents.find(doc => doc.id === id);
-            document.getElementById('documentTitle').value = currentEditingDoc.title;
-            editor.setMarkdown(currentEditingDoc.content);
-            showEditor();
-        }
-
-        async function deleteDocument(id) {
-            if (confirm('确定要删除这个文档吗?')) {
-                try {
-                    const response = await fetch(`/api/documentation/${id}`, { method: 'DELETE' });
-                    if (response.ok) {
-                        showAlert('文档删除成功', 'success');
-                        loadDocumentList();
-                    } else {
-                        throw new Error('删除失败');
-                    }
-                } catch (error) {
-                    console.error('删除文档失败:', error);
-                    showAlert('删除文档失败: ' + error.message, 'error');
-                }
-            }
-        }
-
-        async function togglePublish(id) {
-            try {
-                const response = await fetch(`/api/documentation/${id}/toggle-publish`, { method: 'POST' });
-                if (response.ok) {
-                    loadDocumentList();
-                } else {
-                    throw new Error('操作失败');
-                }
-            } catch (error) {
-                console.error('更改发布状态失败:', error);
-                showAlert('更改发布状态失败: ' + error.message, 'error');
-            }
-        }
-
-
-        function getMenuItems() {
-            return menuItems;
-        }
-
-        function setupMenuDeleteButtons() {
-            const deleteButtons = document.querySelectorAll('.menu-delete-btn');
-            deleteButtons.forEach((button) => {
-                button.addEventListener('click', () => {
-                    const row = button.closest('tr');
-                    const index = parseInt(row.getAttribute('data-index'));
-                    const menuText = row.querySelector('.menu-text').value;
-                    if (confirm(`确定要删除菜单项 "${menuText}" 吗?`)) {
-                        deleteMenuItem(index);
-                    }
-                });
-            });
-        }
-
-        function renderMenuItems() {
-            const tbody = document.getElementById('menuTableBody');
-            tbody.innerHTML = '';
-            menuItems.forEach((item, index) => {
-                const row = `
-                    <tr data-index="${index}">
-                        <td><input type="text" class="menu-text" value="${item.text}" disabled></td>
-                        <td><input type="url" class="menu-link" value="${item.link || ''}" disabled></td>
-                        <td>
-                            <select class="menu-newtab" disabled>
-                                <option value="false" ${item.newTab ? '' : 'selected'}>否</option>
-                                <option value="true" ${item.newTab ? 'selected' : ''}>是</option>
-                            </select>
-                        </td>
-                        <td>
-                            <button type="button" class="action-btn edit-btn">编辑</button>
-                            <button type="button" class="action-btn delete-btn menu-delete-btn">删除</button>
-                        </td>
-                    </tr>
-                `;
-                tbody.innerHTML += row;
-            });
-            setupEditButtons();
-            setupMenuDeleteButtons();
-        }
-
-        function setMenuItems(items) {
-            menuItems = items;
-            renderMenuItems();
-        }
-
-        function setupEditButtons() {
-            const editButtons = document.querySelectorAll('.edit-btn');
-            editButtons.forEach((button, index) => {
-                button.addEventListener('click', () => {
-                    const row = button.closest('tr');
-                    const textInput = row.querySelector('.menu-text');
-                    const linkInput = row.querySelector('.menu-link');
-                    const newTabSelect = row.querySelector('.menu-newtab');
-
-                    if (textInput.disabled) {
-                        textInput.disabled = false;
-                        linkInput.disabled = false;
-                        newTabSelect.disabled = false;
-                        button.textContent = '保存';
-                    } else {
-                        const text = textInput.value;
-                        const link = linkInput.value;
-                        const newTab = newTabSelect.value === 'true';
-
-                        if (text) {
-                            const rowIndex = row.getAttribute('data-index');
-                            menuItems[rowIndex] = { text, link, newTab };
-                            saveMenuItem(rowIndex, { text, link, newTab });
-                            renderMenuItems(); // 重新渲染菜单项
-                        } else {
-                            showAlert('请填写菜单项文本', 'error');
-                        }
-                    }
-                });
-            });
-        }
-        
-
-        function showNewMenuItemRow() {
-            const tbody = document.getElementById('menuTableBody');
-            const newRow = `
-                <tr id="newMenuItemRow">
-                    <td><input type="text" class="menu-text" placeholder="菜单项文本"></td>
-                    <td><input type="url" class="menu-link" placeholder="菜单项链接 (可选)"></td>
-                    <td>
-                        <select class="menu-newtab">
-                            <option value="false">否</option>
-                            <option value="true">是</option>
-                        </select>
-                    </td>
-                    <td>
-                        <button type="button" class="action-btn" onclick="saveNewMenuItem()">保存</button>
-                        <button type="button" class="action-btn" onclick="cancelNewMenuItem()">取消</button>
-                    </td>
-                </tr>
-            `;
-            tbody.insertAdjacentHTML('beforeend', newRow);
-        }
-
-
-        function saveNewMenuItem() {
-            const newRow = document.getElementById('newMenuItemRow');
-            const textInput = newRow.querySelector('.menu-text');
-            const linkInput = newRow.querySelector('.menu-link');
-            const newTabSelect = newRow.querySelector('.menu-newtab');
-
-            const text = textInput.value;
-            const link = linkInput.value;
-            const newTab = newTabSelect.value === 'true';
-
-            if (text) {
-                const newItem = { text, link, newTab };
-                menuItems.push(newItem);
-                renderMenuItems(); // 先更新页面
-                saveMenuItem(menuItems.length - 1, newItem);
-                cancelNewMenuItem();
-            } else {
-                showAlert('请填写菜单项文本', 'error');
-            }
-        }
-
-        function cancelNewMenuItem() {
-            const newRow = document.getElementById('newMenuItemRow');
-            if (newRow) {
-                newRow.remove();
-            }
-        }
-
-        async function saveLogo() {
-            const logoUrl = document.getElementById('logoUrl').value;
-            if (!logoUrl) {
-                showAlert('Logo URL 不可为空', 'error');
-                return;
-            }
-            try {
-                await saveConfig({ logo: logoUrl });
-                showAlert('Logo 保存成功', 'success');
-            } catch (error) {
-                showAlert('Logo 保存失败: ' + error.message, 'error');
-            }
-        }
-
-        async function saveProxyDomain() {
-            const proxyDomain = document.getElementById('proxyDomain').value;
-            if (!proxyDomain) {
-                showAlert('Docker镜像代理地址不可为空', 'error');
-                return;
-            }
-            try {
-                await saveConfig({ proxyDomain });
-                showAlert('代理地址保存成功', 'success');
-            } catch (error) {
-                showAlert('代理地址保存失败: ' + error.message, 'error');
-            }
-        }
-
-        async function saveMenuItem(index, item) {
-            const config = { menuItems: menuItems };
-            config.menuItems[index] = item;
-            await saveConfig(config);
-        }
-
-        async function deleteMenuItem(index) {
-            try {
-                menuItems.splice(index, 1);
-                const response = await fetch('/api/config', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ menuItems: menuItems })
-                });
-                if (response.ok) {
-                    console.log('Menu item deleted successfully');
-                    renderMenuItems(); // 重新渲染菜单项
-                } else {
-                    throw new Error('Failed to delete menu item');
-                }
-            } catch (error) {
-                console.error('删除菜单项失败:', error);
-                showAlert('删除菜单项失败: ' + error.message, 'error');
-            }
-        }
-
-        async function saveConfig(partialConfig) {
-            try {
-                const response = await fetch('/api/config', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify(partialConfig)
-                });
-                if (!response.ok) {
-                    throw new Error('保存失败');
-                }
-            } catch (error) {
-                console.error('保存失败: ' + error.message);
-                throw error;
-            }
-        }
-
-        // Fix 6: Ad-related code has been removed
-
-        async function refreshCaptcha() {
-            try {
-                const response = await fetch('/api/captcha');
-                const data = await response.json();
-                document.getElementById('captchaText').textContent = data.captcha;
-            } catch (error) {
-                console.error('刷新验证码失败:', error);
-            }
-        }
-
-        async function refreshDockerStatus() {
-            const spinner = document.getElementById('loadingSpinner');
-            const refreshButton = document.getElementById('refreshDockerStatusButton');
-            const table = document.getElementById('dockerStatusTable');
-            
-            try {
-                spinner.style.display = 'block';
-                refreshButton?.classList.add('disabled');
-                table.classList.add('disabled');
-                
-                await loadDockerStatus();
-            } catch (error) {
-                console.error('刷新 Docker 状态失败:', error);
-                showAlert('刷新 Docker 状态失败: ' + error.message, 'error');
-            } finally {
-                spinner.style.display = 'none';
-                refreshButton?.classList.remove('disabled');
-                table.classList.remove('disabled');
-            }
-        }
-
-        function saveDockerStatusToCache(containerStatus) {
-            localStorage.setItem('dockerStatus', JSON.stringify(containerStatus));
-            localStorage.setItem('dockerStatusTimestamp', Date.now());
-        }
-
-        function getDockerStatusFromCache() {
-            const cachedStatus = localStorage.getItem('dockerStatus');
-            const timestamp = localStorage.getItem('dockerStatusTimestamp');
-            if (cachedStatus && timestamp) {
-                // 检查缓存是否在过去5分钟内更新过
-                if (Date.now() - parseInt(timestamp) < 5 * 60 * 1000) {
-                    return JSON.parse(cachedStatus);
-                }
-            }
-            return null;
-        }
-
-        let isDockerStatusLoaded = false;
-        
-        async function loadDockerStatus() {
-            const tbody = document.getElementById('dockerStatusTableBody');
-            
-            // 尝试从缓存加载数据
-            const cachedStatus = getDockerStatusFromCache();
-            if (cachedStatus) {
-                renderDockerStatus(cachedStatus);
-                isDockerStatusLoaded = true;
-            } else if (!isDockerStatusLoaded) {
-                tbody.innerHTML = '<tr><td colspan="8" style="text-align: center;">加载中...</td></tr>';
-            }
-
-            try {
-                const response = await fetch('/api/docker-status');
-                if (!response.ok) {
-                    if (response.status === 503) {
-                        throw new Error('无法连接到 Docker 守护进程');
-                    }
-                    throw new Error('Failed to fetch Docker status');
-                }
-                const containerStatus = await response.json();
-                renderDockerStatus(containerStatus);
-                isDockerStatusLoaded = true;
-                saveDockerStatusToCache(containerStatus);
-            } catch (error) {
-                console.error('Error loading Docker status:', error);
-                if (!cachedStatus) {
-                    tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
-                }
-                isDockerStatusLoaded = false;
-            }
-        }
-
-        function renderDockerStatus(containerStatus) {
-            const table = document.getElementById('dockerStatusTable');
-            const thead = table.getElementsByTagName('thead')[0];
-            const tbody = document.getElementById('dockerStatusTableBody');
-
-            thead.innerHTML = `
-                <tr>
-                    <th>容器 ID</th>
-                    <th>名称</th>
-                    <th>镜像</th>
-                    <th>状态</th>
-                    <th>CPU</th>
-                    <th>内存</th>
-                    <th>创建时间</th>
-                    <th>操作</th>
-                </tr>
-            `;
-
-            tbody.innerHTML = '';
-
-            containerStatus.forEach(container => {
-                const row = `
-                <tr>
-                    <td>${container.id}</td>
-                    <td>${container.name}</td>
-                    <td>${container.image}</td>
-                    <td class="status-cell" id="status-${container.id}">
-                        <div class="status-content">${container.state}</div>
-                    </td>
-                    <td>${container.cpu}</td>
-                    <td>${container.memory}</td>
-                    <td>${container.created}</td>
-                    <td>
-                        <select onchange="handleContainerAction('${container.id}', '${container.image}', this.value)" class="action-select">
-                            <option value="">选择操作</option>
-                            <option value="restart">重启</option>
-                            <option value="stop">停止</option>
-                            <option value="update">更新</option>
-                            <option value="delete">删除</option>
-                            <option value="logs">查看日志</option>                            
-                        </select>
-                    </td>
-                </tr>
-                `;
-                tbody.innerHTML += row;
-            });
-        }
-
-        function showAlert(message, type = 'info') {
-            Swal.fire({
-                title: type === 'error' ? '错误' : '提示',
-                text: message,
-                icon: type,
-                confirmButtonText: '确定',
-                confirmButtonColor: '#3D7CF4'
-            });
-        }
-
-        function showConfirm(message, callback) {
-            Swal.fire({
-                title: '确认操作',
-                text: message,
-                icon: 'question',
-                showCancelButton: true,
-                confirmButtonText: '确定',
-                cancelButtonText: '取消',
-                confirmButtonColor: '#3D7CF4',
-                cancelButtonColor: '#6C757D'
-            }).then((result) => {
-                if (result.isConfirmed) {
-                    callback();
-                }
-            });
-        }
-
-        function handleContainerAction(id, image, action) {
-            if (action === '') return;
-            
-            switch(action) {
-                case 'restart':
-                    showConfirm(`确定要重启容器 ${id} 吗?`, () => restartContainer(id));
-                    break;
-                case 'stop':
-                    showConfirm(`确定要停止容器 ${id} 吗?`, () => stopContainer(id));
-                    break;
-                case 'update':
-                    updateContainer(id, image);
-                    break;
-                case 'logs':
-                    viewLogs(id);
-                    break;
-                case 'delete':
-                    showConfirm(`确定要删除容器 ${id} 吗?此操作不可逆。`, () => deleteContainer(id));
-                    break;
-            }
-            
-            document.querySelector(`select[onchange*="${id}"]`).value = "";
-        }
-
-        async function viewLogs(id) {
-            try {
-                // 创建模态框
-                const modal = document.createElement('div');
-                modal.style.position = 'fixed';
-                modal.style.left = '0';
-                modal.style.top = '0';
-                modal.style.width = '100%';
-                modal.style.height = '100%';
-                modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
-                modal.style.display = 'flex';
-                modal.style.justifyContent = 'center';
-                modal.style.alignItems = 'center';
-                modal.style.zIndex = '1000';
-
-                const content = document.createElement('div');
-                content.style.backgroundColor = 'black';
-                content.style.color = 'white';
-                content.style.padding = '20px';
-                content.style.borderRadius = '5px';
-                content.style.width = '80%';
-                content.style.height = '80%';
-                content.style.display = 'flex';
-                content.style.flexDirection = 'column';
-                content.style.position = 'relative';
-
-                const logContent = document.createElement('pre');
-                logContent.style.flex = '1';
-                logContent.style.overflowY = 'auto';
-                logContent.style.padding = '10px';
-                logContent.style.backgroundColor = '#1e1e1e';
-                logContent.style.color = '#d4d4d4';
-                logContent.style.fontFamily = 'monospace';
-                logContent.style.fontSize = '14px';
-                logContent.style.lineHeight = '1.5';
-                logContent.style.whiteSpace = 'pre-wrap';
-                logContent.style.wordBreak = 'break-all';
-
-                const closeButton = document.createElement('button');
-                closeButton.textContent = '关闭';
-                closeButton.style.position = 'absolute';
-                closeButton.style.top = '10px';
-                closeButton.style.right = '10px';
-                closeButton.style.padding = '5px 10px';
-                closeButton.style.backgroundColor = '#4CAF50';
-                closeButton.style.color = 'white';
-                closeButton.style.border = 'none';
-                closeButton.style.borderRadius = '3px';
-                closeButton.style.cursor = 'pointer';
-
-                content.appendChild(logContent);
-                content.appendChild(closeButton);
-                modal.appendChild(content);
-                document.body.appendChild(modal);
-
-                // 使用 WebSocket 或长轮询获取日志
-                const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-                let ws;
-
-                try {
-                    ws = new WebSocket(`${protocol}//${window.location.host}/api/docker/logs/${id}`);
-                    
-                    ws.onopen = () => {
-                        logContent.textContent += "WebSocket 连接已建立,正在接收日志...\n";
-                    };
-
-                    ws.onmessage = (event) => {
-                        const filteredData = event.data.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '');
-                        logContent.textContent += filteredData;
-                        logContent.scrollTop = logContent.scrollHeight;
-                    };
-
-                    ws.onerror = (error) => {
-                        console.error('WebSocket错误:', error);
-                        logContent.textContent += "WebSocket 连接错误,切换到长轮询模式...\n";
-                        useLongPolling(id, logContent);
-                    };
-
-                    ws.onclose = () => {
-                        logContent.textContent += "WebSocket 连接已关闭。\n";
-                    };
-                } catch (error) {
-                    console.error('WebSocket连接失败:', error);
-                    logContent.textContent += "无法建立 WebSocket 连接,使用长轮询模式...\n";
-                    useLongPolling(id, logContent);
-                }
-
-                // 关闭按钮和模态框点击事件
-                closeButton.onclick = () => {
-                    if (ws) ws.close();
-                    document.body.removeChild(modal);
-                };
-
-                modal.onclick = (e) => {
-                    if (e.target === modal) {
-                        if (ws) ws.close();
-                        document.body.removeChild(modal);
-                    }
-                };
-
-            } catch (error) {
-                console.error('查看日志失败:', error);
-                showAlert('查看日志失败: ' + error.message, 'error');
-            }
-        }
-
-        function useLongPolling(id, logContent) {
-            let isPolling = true;
-
-            async function pollLogs() {
-                if (!isPolling) return;
-
-                try {
-                    const response = await fetch(`/api/docker/logs-poll/${id}`);
-                    if (!response.ok) throw new Error('获取日志失败');
-                    
-                    const logs = await response.text();
-                    logContent.textContent += logs;
-                    logContent.scrollTop = logContent.scrollHeight;
-                } catch (error) {
-                    console.error('轮询日志失败:', error);
-                    logContent.textContent += "获取日志失败,请检查网络连接...\n";
-                }
-
-                // 继续轮询
-                setTimeout(pollLogs, 2000);
-            }
-
-            pollLogs();
-
-            // 返回一个停止轮询的函数
-            return () => { isPolling = false; };
-        }
-
-
-        async function restartContainer(id) {
-            try {
-                const statusCell = document.getElementById(`status-${id}`);
-                statusCell.innerHTML = '<div class="loading-container"><div class="loading"></div></div>';
-
-                const response = await fetch(`/api/docker/restart/${id}`, { method: 'POST' });
-                if (response.ok) {
-                    await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
-                    const newStatus = await getContainerStatus(id);
-                    statusCell.textContent = newStatus;
-                } else {
-                    throw new Error('重启失败');
-                }
-            } catch (error) {
-                console.error('重启容器失败:', error);
-                showAlert('重启容器失败: ' + error.message, 'error');
-                loadDockerStatus(); // 重新加载所有容器状态
-            }
-        }
-
-        async function stopContainer(id) {
-            try {
-                const statusCell = document.getElementById(`status-${id}`);
-                statusCell.innerHTML = '<div class="loading-container"><div class="loading"></div></div>';
-                
-                const response = await fetch(`/api/docker/stop/${id}`, { method: 'POST' });
-                if (response.ok) {
-                    await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
-                    const newStatus = await getContainerStatus(id);
-                    statusCell.innerHTML = `<div class="status-content">${newStatus}</div>`;
-                } else {
-                    throw new Error('停止失败');
-                }
-            } catch (error) {
-                console.error('停止容器失败:', error);
-                showAlert('停止容器失败: ' + error.message, 'error');
-                loadDockerStatus(); // 重新加载所有容器状态
-            }
-        }
-
-        async function getContainerStatus(id) {
-            const response = await fetch(`/api/docker/status/${id}`);
-            if (response.ok) {
-                const data = await response.json();
-                return data.state;
-            } else {
-                throw new Error('获取容器状态失败');
-            }
-        }
-
-
-        async function updateContainer(id, currentImage) {
-            const tag = prompt(`请输入 ${currentImage} 的新标签:`, 'latest');
-            if (tag) {
-                try {
-                    const statusCell = document.getElementById(`status-${id}`);
-                    statusCell.textContent = 'Updating';
-                    statusCell.style.color = 'orange';
-
-                    const response = await fetch(`/api/docker/update/${id}`, {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ tag })
-                    });
-
-                    if (response.ok) {
-                        const result = await response.json();
-                        showAlert(result.message || '容器更新成功', 'success');
-                    } else {
-                        const errorData = await response.json();
-                        throw new Error(errorData.error || '更新失败');
-                    }
-                } catch (error) {
-                    console.error('更新容器失败:', error);
-                    showAlert('更新容器失败: ' + error.message, 'error');
-                } finally {
-                    loadDockerStatus(); // 重新加载容器状态
-                }
-            }
-        }
-
-        function refreshDockerStatus() {
-            isDockerStatusLoaded = false;
-            localStorage.removeItem('dockerStatus');
-            localStorage.removeItem('dockerStatusTimestamp');
-            loadDockerStatus();
-        }
-        async function deleteContainer(id) {
-            try {
-                const statusCell = document.getElementById(`status-${id}`);
-                statusCell.textContent = 'Deleting';
-                statusCell.style.color = 'red';
-
-                const response = await fetch(`/api/docker/delete/${id}`, { method: 'POST' });
-                if (response.ok) {
-                    showAlert('容器删除成功', 'success');
-                    loadDockerStatus(); // 重新加载容器状态
-                } else {
-                    const errorData = await response.json();
-                    throw new Error(errorData.error || '删除失败');
-                }
-            } catch (error) {
-                console.error('删除容器失败:', error);
-                showAlert('删除容器失败: ' + error.message, 'error');
-                loadDockerStatus(); // 重新加载所有容器状态
-            }
-        }
+    
+    <!-- 添加 Bootstrap 5 JavaScript -->
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
+    
+    <!-- Toast UI Editor - 添加编辑器需要的依赖 -->
+    <script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
+    <link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />
+    
+    <!-- 模块化JS文件 - 按依赖顺序加载 -->
+    <script src="js/core.js"></script>
+    <script src="js/auth.js"></script>
+    <script src="js/userCenter.js"></script>
+    <script src="js/menuManager.js"></script>
+    <script src="js/documentManager.js"></script>
+    <script src="js/systemStatus.js"></script>
+    <script src="js/dockerManager.js"></script>
+    <script src="js/networkTest.js"></script>
+    <script src="js/app.js"></script>
 
+    <script>
+        // 在DOM加载完成后初始化登录表单
         document.addEventListener('DOMContentLoaded', function() {
-            const sidebarItems = document.querySelectorAll('.sidebar li');
-            const contentSections = document.querySelectorAll('.content-section');
-
-            // 只使用domainSelect
-            const domainSelect = document.getElementById('domainSelect');
-
-            // 网络测试函数
-            function runNetworkTest() {
-                const domain = domainSelect.value;
-                const testType = document.getElementById('testType').value;
-                const resultsDiv = document.getElementById('testResults');
-
-                // 验证选择了域名
-                if (!domain) {
-                    showAlert('请选择目标域名', 'error');
-                    return;
-                }
-
-                resultsDiv.innerHTML = '测试中,请稍候...';
-
-                const controller = new AbortController();
-                const timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时
-
-                fetch('/api/network-test', {
-                    method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json',
-                    },
-                    body: JSON.stringify({ domain, type: testType }),
-                    signal: controller.signal
-                })
-                .then(response => {
-                    clearTimeout(timeoutId);
-                    if (!response.ok) {
-                        throw new Error('网络测试失败');
-                    }
-                    return response.text();
-                })
-                .then(result => {
-                    resultsDiv.textContent = result;
-                })
-                .catch(error => {
-                    console.error('网络测试出错:', error);
-                    if (error.name === 'AbortError') {
-                        resultsDiv.textContent = '测试超时,请稍后再试';
-                    } else {
-                        resultsDiv.textContent = '测试失败: ' + error.message;
-                    }
+            const loginForm = document.getElementById('loginForm');
+            if (loginForm) {
+                loginForm.addEventListener('submit', function(e) {
+                    e.preventDefault();
+                    auth.login();
                 });
             }
 
-            // 绑定测试按钮点击事件
-            document.querySelector('#network-test button').addEventListener('click', runNetworkTest);
-
-            // docker监控
-            loadContainers();
-            loadMonitoringConfig();
-            
-            // 绑定测试通知按钮的点击事件
-            document.querySelector('#docker-monitoring button:nth-of-type(1)').addEventListener('click', testNotification);
-            
-            // 绑定保存配置按钮的点击事件
-            document.querySelector('#docker-monitoring button:nth-of-type(2)').addEventListener('click', saveMonitoringConfig);
-            
-            // 绑定开启/关闭监控按钮的点击事件
-            document.querySelector('#docker-monitoring button:nth-of-type(3)').addEventListener('click', toggleMonitoring);
-
-            // 为通知类型下拉框添加变更事件监听器
-            document.getElementById('notificationType').addEventListener('change', toggleNotificationFields);
-
-            refreshStoppedContainers(); // 初始加载已停止的容器
-
-            // 侧边栏点击事件 - 使用全局的showSection函数
-            sidebarItems.forEach(item => {
-                item.addEventListener('click', function() {
-                    const sectionId = this.getAttribute('data-section');
-                    showSection(sectionId);
+            const changePasswordForm = document.getElementById('changePasswordForm');
+            if (changePasswordForm) {
+                changePasswordForm.addEventListener('submit', function(e) {
+                    e.preventDefault();
+                    userCenter.changePassword();
                 });
-            });
-
-
-            // docker 监控
-            // 显示消息
-            function showMessage(message, isError = false) {
-                const messageContainer = document.getElementById('messageContainer');
-                const messageElement = document.createElement('div');
-                messageElement.textContent = message;
-                messageElement.className = isError ? 'error-message' : 'success-message';
-                messageContainer.appendChild(messageElement);
-                setTimeout(() => messageElement.remove(), 3000);
             }
-
-            // 加载容器列表
-            async function loadContainers() {
-                try {
-                    const response = await fetch('/api/stopped-containers');
-                    const containers = await response.json();
-                    renderStoppedContainers(containers);
-                } catch (error) {
-                    showMessage('加载容器列表失败: ' + error.message, true);
-                }
-            }
-            // 确保在页面加载时调用 loadContainers
-            document.addEventListener('DOMContentLoaded', loadContainers);
-
-            // 切换单个容器的监控状态
-            async function toggleContainerMonitoring(containerId, isMonitored) {
-                try {
-                    const response = await fetch(`/api/container/${containerId}/monitor`, {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ isMonitored })
-                    });
-                    if (response.ok) {
-                        showMessage(`容器 ${containerId} 监控状态已${isMonitored ? '开启' : '关闭'}`);
-                    } else {
-                        throw new Error('操作失败');
-                    }
-                } catch (error) {
-                    showMessage(`切换容器监控状态失败: ${error.message}`, true);
-                }
-            }
-
-            function toggleNotificationFields() {
-                const notificationType = document.getElementById('notificationType').value;
-                const wechatFields = document.getElementById('wechatFields');
-                const telegramFields = document.getElementById('telegramFields');
-
-                if (notificationType === 'wechat') {
-                    wechatFields.style.display = 'block';
-                    telegramFields.style.display = 'none';
-                } else if (notificationType === 'telegram') {
-                    wechatFields.style.display = 'none';
-                    telegramFields.style.display = 'block';
-                }
-            }
-
-            async function testNotification() {
-                const notificationType = document.getElementById('notificationType').value;
-                let data = {
-                    notificationType: notificationType,
-                    monitorInterval: document.getElementById('monitorInterval').value
-                };
-
-                if (notificationType === 'wechat') {
-                    data.webhookUrl = document.getElementById('webhookUrl').value;
-                } else if (notificationType === 'telegram') {
-                    data.telegramToken = document.getElementById('telegramToken').value;
-                    data.telegramChatId = document.getElementById('telegramChatId').value;
-                }
-
-                try {
-                    const response = await fetch('/api/test-notification', {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify(data)
-                    });
-
-                    if (response.ok) {
-                        const result = await response.json();
-                        showMessage(result.message || '通知测试成功!');
-                    } else {
-                        const errorData = await response.json();
-                        throw new Error(errorData.error || '测试失败');
-                    }
-                } catch (error) {
-                    showMessage('通知测试失败: ' + error.message, true);
-                }
-            }
-
-            // 修改保存配置的函数
-            async function saveMonitoringConfig() {
-                const notificationType = document.getElementById('notificationType').value;
-                let data = {
-                    notificationType: notificationType,
-                    monitorInterval: document.getElementById('monitorInterval').value,
-                    isEnabled: document.getElementById('monitoringStatus').textContent === '已开启',
-                    webhookUrl: document.getElementById('webhookUrl').value,
-                    telegramToken: document.getElementById('telegramToken').value,
-                    telegramChatId: document.getElementById('telegramChatId').value
-                };
-
-                try {
-                    const response = await fetch('/api/monitoring-config', {
-                        method: 'POST',
-                        headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify(data)
-                    });
-                    if (response.ok) {
-                        showMessage('监控配置已保存');
-                    } else {
-                        throw new Error('保存失败');
-                    }
-                } catch (error) {
-                    showMessage('保存监控配置失败: ' + error.message, true);
-                }
-            }
-
-            async function toggleMonitoring() {
-                const currentStatus = document.getElementById('monitoringStatus').textContent;
-                const newStatus = currentStatus === '已开启' ? false : true;
-
-                try {
-                    const response = await fetch('/api/toggle-monitoring', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ isEnabled: newStatus })
-                    });
-                    if (response.ok) {
-                    const result = await response.json();
-                    if (result.success) {
-                        updateMonitoringStatus(newStatus);
-                        showMessage(newStatus ? '监控已开启' : '监控已关闭');
-                    } else {
-                        throw new Error(result.message || '操作失败');
-                    }
-                    } else {
-                    throw new Error('切换失败');
-                    }
-                } catch (error) {
-                    showMessage('切换监控状态失败: ' + error.message, true);
-                }
-            }
-
-            // 加载监控配置
-            async function loadMonitoringConfig() {
-                try {
-                    const response = await fetch('/api/monitoring-config');
-                    const config = await response.json();
-                    
-                    document.getElementById('notificationType').value = config.notificationType || 'wechat';
-                    document.getElementById('webhookUrl').value = config.webhookUrl || '';
-                    document.getElementById('telegramToken').value = config.telegramToken || '';
-                    document.getElementById('telegramChatId').value = config.telegramChatId || '';
-                    document.getElementById('monitorInterval').value = config.monitorInterval || 60;
-                    
-                    updateMonitoringStatus(config.isEnabled);
-                    toggleNotificationFields(); // 确保根据加载的配置显示正确的字段
-                } catch (error) {
-                    showMessage('加载监控配置失败: ' + error.message, true);
-                }
-            }
-
-            // 更新监控状态显示
-            function updateMonitoringStatus(isEnabled) {
-                const statusElement = document.getElementById('monitoringStatus');
-                statusElement.textContent = isEnabled ? '已开启' : '已关闭';
-                statusElement.style.color = isEnabled ? 'green' : 'red';
-                document.getElementById('toggleMonitoringBtn').textContent = isEnabled ? '关闭监控' : '开启监控';
-            }
-
-            async function refreshStoppedContainers() {
-                const spinner = document.getElementById('loadingSpinner');
-                const refreshButton = document.querySelector('#docker-monitoring button:last-child');
-                const table = document.getElementById('stoppedContainersTable');
-                
-                try {
-                    spinner.style.display = 'block';
-                    refreshButton.disabled = true;
-                    table.classList.add('disabled');
-                    
-                    const response = await fetch('/api/refresh-stopped-containers');
-                    if (!response.ok) {
-                        throw new Error('Failed to fetch stopped containers');
-                    }
-                    const containers = await response.json();
-                    renderStoppedContainers(containers);
-                    showMessage('已停止的容器状态已刷新', false);
-                } catch (error) {
-                    console.error('Error refreshing stopped containers:', error);
-                    showMessage('刷新已停止的容器状态失败: ' + error.message, true);
-                } finally {
-                    spinner.style.display = 'none';
-                    refreshButton.disabled = false;
-                    table.classList.remove('disabled');
-                }
-            }
-
-            function renderStoppedContainers(containers) {
-                const tbody = document.getElementById('stoppedContainersBody');
-                tbody.innerHTML = '';
-
-                if (containers.length === 0) {
-                    tbody.innerHTML = '<tr><td colspan="3">没有已停止的容器</td></tr>';
-                    return;
-                }
-
-                containers.forEach(container => {
-                    const row = `
-                        <tr>
-                            <td>${container.id}</td>
-                            <td>${container.name}</td>
-                            <td>${container.status}</td>
-                        </tr>
-                    `;
-                    tbody.innerHTML += row;
-                });
-            }
-
-            // 确保在页面加载时初始化停止的容器列表
-            document.addEventListener('DOMContentLoaded', () => {
-                loadMonitoringConfig();
-                document.getElementById('notificationType').addEventListener('change', toggleNotificationFields);
-            });
-        });
-
-        async function login() {
-            const username = document.getElementById('username').value;
-            const password = document.getElementById('password').value;
-            const captcha = document.getElementById('captcha').value;
             
-            try {
-                document.getElementById('loadingSpinner').style.display = 'block';
-                
-                const response = await fetch('/api/login', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ username, password, captcha })
-                });
-                
-                if (response.ok) {
-                    isLoggedIn = true;
-                    localStorage.setItem('isLoggedIn', 'true');
-                    persistSession();
-                    
-                    document.getElementById('loginModal').style.display = 'none';
-                    document.getElementById('adminContainer').style.display = 'flex';
-                    
-                    document.getElementById('currentUsername').textContent = username;
-                    document.getElementById('welcomeUsername').textContent = username;
-                    
-                    await loadConfig();
-                    loadDocumentList();
-                    initEditor();
-                    
-                    bindUserActionButtons();
-                    
-                    bindDashboardCardActions();
-                    
-                    refreshSystemStatus();
-                    loadUserStats();
-                    loadDockerStatus();
-
-                    showSection('dashboard');
-                } else {
-                    const errorData = await response.json();
-                    showAlert(errorData.error || '登录失败', 'error');
-                }
-            } catch (error) {
-                showAlert('登录失败: ' + error.message, 'error');
-            } finally {
-                document.getElementById('loadingSpinner').style.display = 'none';
-            }
-        }
-
-        async function loadConfig() {
-            try {
-                const response = await fetch('/api/config');
-                const config = await response.json();
-                document.getElementById('logoUrl').value = config.logo || '';
-                document.getElementById('proxyDomain').value = config.proxyDomain || '';
-                setMenuItems(config.menuItems || []);
-            } catch (error) {
-                console.error('加载配置失败:', error);
-            }
-        }
-
-        // Fix 5: Improved session handling for logout
-        async function logout() {
-            console.log("注销操作被触发");
-            try {
-                // Show loading indicator
-                document.getElementById('loadingSpinner').style.display = 'block';
-                
-                const response = await fetch('/api/logout', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' }
-                });
-                
-                if (response.ok) {
-                    // Fix 5: Clear both localStorage and sessionStorage
-                    localStorage.removeItem('isLoggedIn');
-                    sessionStorage.removeItem('sessionActive');
-                    
-                    // Clean up cookies if possible
-                    document.cookie = 'connect.sid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
-                    
-                    // Reload page to show login screen
-                    window.location.reload();
-                } else {
-                    throw new Error('退出登录失败');
-                }
-            } catch (error) {
-                console.error('退出登录失败:', error);
-                showAlert('退出登录失败: ' + error.message, 'error');
-                document.getElementById('loadingSpinner').style.display = 'none';
-            }
-        }
-
-        async function changePassword() {
-            const currentPassword = document.getElementById('currentPassword').value;
-            const newPassword = document.getElementById('newPassword').value;
-            const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
-
-            if (!currentPassword || !newPassword) {
-                showAlert('请填写当前密码和新密码', 'error');
-                return;
-            }
-            if (!passwordRegex.test(newPassword)) {
-                showAlert('密码必须包含至少一个字母、一个数字和一个特殊字符,长度在8到16个字符之间', 'error');
-                return;
-            }
-            try {
-                const response = await fetch('/api/change-password', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ currentPassword, newPassword })
-                });
-                if (response.ok) {
-                    showAlert('密码已修改,即将退出登录', 'success');
-                    
-                    // 延迟一会儿再清除会话
-                    setTimeout(() => {
-                        // 清除当前会话并显示登录模态框
-                        localStorage.removeItem('isLoggedIn');
-                        sessionStorage.removeItem('sessionActive');
-                        isLoggedIn = false;
-                        document.getElementById('loginModal').style.display = 'block';
-                        document.getElementById('adminContainer').style.display = 'none';
-                        refreshCaptcha();
-
-                        // 清除登录表单中的输入数据
-                        document.getElementById('username').value = '';
-                        document.getElementById('password').value = '';
-                        document.getElementById('captcha').value = '';
-                    }, 1500);
-                } else {
-                    showAlert('修改密码失败', 'error');
-                }
-            } catch (error) {
-                showAlert('修改密码失败: ' + error.message, 'error');
-            }
-        }
-
-        function checkPasswordStrength() {
-            const newPassword = document.getElementById('newPassword').value;
-            const passwordHint = document.getElementById('passwordHint');
-
-            const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
-
-            if (!passwordRegex.test(newPassword)) {
-                passwordHint.style.display = 'block';
-            } else {
-                passwordHint.style.display = 'none';
-            }
-        }
-
-        async function refreshSystemStatus() {
-            console.log("刷新系统状态");
-            try {
-                // 首先获取系统整体状态
-                const response = await fetch('/api/system-stats');
-                let data;
-                
-                if (response.ok) {
-                    data = await response.json();
-                    console.log("接收到的系统状态数据:", data);
-                } else {
-                    // 如果API请求失败,创建一个带警告信息的默认数据对象
-                    console.warn("系统状态API返回错误:", response.status);
-                    data = {
-                        dockerAvailable: false,
-                        containerCount: '0',
-                        memoryUsage: '未知',
-                        cpuLoad: '未知',
-                        diskSpace: '未知',
-                        recentActivities: [{
-                            time: new Date().toLocaleString(),
-                            action: 'API错误',
-                            container: '系统',
-                            status: `错误代码: ${response.status}`
-                        }]
-                    };
-                    
-                    // 如果是503错误,显示Docker服务不可用
-                    if (response.status === 503) {
-                        showAlert('Docker守护进程不可用,某些功能可能受限', 'warning');
-                    } else {
-                        showAlert('获取系统状态失败,使用默认数据', 'warning');
+            // 设置超时自动显示登录框,避免一直显示"加载中..."
+            setTimeout(function() {
+                const loadingIndicator = document.getElementById('loadingIndicator');
+                if (loadingIndicator && loadingIndicator.style.display !== 'none') {
+                    console.log('超时自动显示登录框');
+                    if (core && typeof core.hideLoadingIndicator === 'function') {
+                        core.hideLoadingIndicator();
                     }
-                }
-                
-                // 更新Docker状态指示器
-                updateDockerStatusIndicator(data.dockerAvailable);
-                
-                // 更新控制面板数据
-                document.getElementById('totalContainers').textContent = data.containerCount || '0';
-                document.getElementById('systemMemory').textContent = data.memoryUsage || '未知';
-                document.getElementById('cpuLoad').textContent = data.cpuLoad || '未知';
-                
-                // 获取磁盘空间的实际数据
-                try {
-                    const diskResponse = await fetch('/api/disk-space');
-                    if (diskResponse.ok) {
-                        const diskData = await diskResponse.json();
-                        // 更新磁盘空间显示为实际使用百分比
-                        document.getElementById('diskSpace').textContent = diskData.usagePercent + '%';
-                        console.log("获取到的磁盘数据:", diskData);
-                    } else {
-                        // 如果获取磁盘数据失败,使用系统状态中的数据或默认值
-                        document.getElementById('diskSpace').textContent = data.diskSpace || '未知';
-                    }
-                } catch (diskError) {
-                    console.error('获取磁盘数据失败:', diskError);
-                    document.getElementById('diskSpace').textContent = data.diskSpace || '未知';
-                }
-                
-                // 更新UI的视觉状态
-                updateDashboardVisualState(data.dockerAvailable);
-                
-                // 更新最近活动表
-                const activitiesBody = document.getElementById('recentActivitiesBody');
-                activitiesBody.innerHTML = '';
-                
-                if (data.recentActivities && data.recentActivities.length > 0) {
-                    data.recentActivities.forEach(activity => {
-                        const row = `
-                            <tr>
-                                <td>${activity.time}</td>
-                                <td>${activity.action}</td>
-                                <td>${activity.container}</td>
-                                <td>${activity.status}</td>
-                            </tr>
-                        `;
-                        activitiesBody.innerHTML += row;
-                    });
-                } else {
-                    activitiesBody.innerHTML = '<tr><td colspan="4" style="text-align: center;">暂无活动记录</td></tr>';
-                }
-            } catch (error) {
-                console.error('刷新系统状态失败:', error);
-                
-                // 更新Docker状态指示器为离线
-                updateDockerStatusIndicator(false);
-                
-                // 更新UI为默认状态
-                document.getElementById('totalContainers').textContent = '0';
-                document.getElementById('systemMemory').textContent = '未知';
-                document.getElementById('cpuLoad').textContent = '未知';
-                document.getElementById('diskSpace').textContent = '未知';
-                
-                // 更新活动表显示错误
-                const activitiesBody = document.getElementById('recentActivitiesBody');
-                activitiesBody.innerHTML = `
-                    <tr>
-                        <td colspan="4" style="text-align: center; color: red;">
-                            系统状态获取失败: ${error.message}
-                        </td>
-                    </tr>
-                `;
-                
-                // 显示错误提示
-                showAlert('刷新系统状态失败: ' + error.message, 'error');
-            }
-        }
- 
-        
-        // 根据Docker可用性更新仪表板视觉状态
-        function updateDashboardVisualState(dockerAvailable) {
-            const cards = document.querySelectorAll('.dashboard-card');
-            
-            cards.forEach(card => {
-                const cardTitle = card.querySelector('.card-title');
-                const cardValue = card.querySelector('.card-value');
-                
-                if (!dockerAvailable && cardTitle && cardValue) {
-                    // 如果Docker不可用,添加视觉提示
-                    card.style.opacity = '0.7';
                     
-                    // 只为容器相关卡片添加提示
-                    if (cardTitle.textContent.includes('容器')) {
-                        // 检查是否已经添加了提示,避免重复添加
-                        if (!cardValue.innerHTML.includes('Docker离线')) {
-                            cardValue.innerHTML += ' <small style="color:orange">(Docker离线)</small>';
+                    const loginModal = document.getElementById('loginModal');
+                    if (loginModal) {
+                        loginModal.style.display = 'flex';
+                        if (window.auth && typeof window.auth.refreshCaptcha === 'function') {
+                            window.auth.refreshCaptcha();
                         }
                     }
-                } else {
-                    // 恢复正常状态
-                    card.style.opacity = '1';
-                    
-                    // 移除已有的离线提示
-                    if (cardValue && cardValue.innerHTML.includes('Docker离线')) {
-                        cardValue.innerHTML = cardValue.innerHTML.replace(/<small.*?<\/small>/g, '');
-                    }
                 }
-            });
-        }
-
-        function updateDockerStatusIndicator(available) {
-            const indicator = document.getElementById('dockerStatusIndicator');
-            const statusText = document.getElementById('dockerStatusText');
-            
-            if (!indicator || !statusText) {
-                console.warn('Docker状态指示器元素不存在');
-                return;
-            }
+            }, 3000); // 3秒后如果仍在加载,则显示登录框
             
-            if (available) {
-                indicator.style.backgroundColor = '#4CAF50';
-                statusText.textContent = 'Docker 在线';
-            } else {
-                indicator.style.backgroundColor = '#F44336';
-                statusText.textContent = 'Docker 离线';
-            }
-        }
-
-        // 检查磁盘空间的函数
-        function checkDiskSpace() {
-            console.log("检查磁盘空间");
-            fetch('/api/disk-space')
-                .then(response => {
-                    if (!response.ok) {
-                        throw new Error('获取磁盘信息失败');
-                    }
-                    return response.json();
-                })
-                .then(data => {
-                    // 更新磁盘空间显示为实际值
-                    document.getElementById('diskSpace').textContent = data.usagePercent + '%';
-                    // 显示更详细的信息
-                    showAlert(`磁盘使用情况: ${data.diskSpace}, 使用率: ${data.usagePercent}%`);
-                })
-                .catch(error => {
-                    console.error('获取磁盘空间信息失败:', error);
-                    showAlert('获取磁盘空间信息失败: ' + error.message, 'error');
-                });
-        }
-        
-        // 加载用户统计数据
-        async function loadUserStats() {
-            try {
-                const response = await fetch('/api/user-info');
-                if (!response.ok) {
-                    throw new Error('获取用户信息失败');
-                }
-                const data = await response.json();
-                
-                // 更新用户统计数据
-                document.getElementById('loginCount').textContent = data.loginCount || '0';
-                document.getElementById('lastLogin').textContent = data.lastLogin || '未知';
-                document.getElementById('accountAge').textContent = data.accountAge || '0';
-                
-                // 更新欢迎信息
-                document.getElementById('welcomeUsername').textContent = data.username || '管理员';
-                document.getElementById('currentUsername').textContent = data.username || '管理员';
-            } catch (error) {
-                console.error('加载用户统计数据失败:', error);
-            }
-        }
-        
-        // 安全设置保存函数
-        function saveSecuritySettings() {
-            console.log("保存安全设置");
-            const loginNotification = document.getElementById('loginNotification').value;
-            showAlert('安全设置已保存', 'success');
-        }
-
-        window.onload = async function() {
-            try {
-                // 改进会话检查逻辑,同时检查localStorage和sessionStorage
-                const response = await fetch('/api/check-session');
-                if (response.ok) {
-                    // 使用双重检查,提高会话持久性
-                    isLoggedIn = localStorage.getItem('isLoggedIn') === 'true' || checkSessionPersistence();
-                    
-                    if (isLoggedIn) {
-                        // 确保会话持久化
-                        persistSession();
-                        document.getElementById('adminContainer').style.display = 'flex';
-                        await loadConfig();
-                        // 加载其他必要数据...
-                        bindUserActionButtons(); // 确保按钮绑定事件
-                        // 继续其他初始化...
-                    } else {
-                        document.getElementById('loginModal').style.display = 'flex';
-                        refreshCaptcha();
-                    }
-                } else {
-                    throw new Error('Session check failed');
-                }
-            } catch (error) {
-                console.error('Error during initialization:', error);
-                // 清除会话数据
-                sessionStorage.removeItem('sessionActive');
-                localStorage.removeItem('isLoggedIn');
-                document.getElementById('loginModal').style.display = 'flex';
-                refreshCaptcha();
-            } finally {
-                document.getElementById('loadingIndicator').style.display = 'none';
-            }
-        };
-
-        // 覆盖默认的 alert 和 confirm 函数,使用 Sweet Alert
-        const originalAlert = window.alert;
-        window.alert = function(message) {
-            showAlert(message, message.toLowerCase().includes('失败') || message.toLowerCase().includes('错误') ? 'error' : 'success');
-        };
-
-        const originalConfirm = window.confirm;
-        window.confirm = function(message) {
-            return new Promise((resolve) => {
-                showConfirm(message, () => resolve(true));
-            });
-        };
+            // 确保saveConfig可用
+            window.saveConfig = window.app ? window.app.saveConfig : function(data) {
+                console.error('saveConfig未定义');
+            };
+        });
     </script>
 </body>
 </html>

+ 53 - 0
hubcmdui/web/compatibility-layer.js

@@ -0,0 +1,53 @@
+// ... existing code ...
+    // 获取文档列表
+    app.get('/api/documentation', requireLogin, async (req, res) => {
+        try {
+            const docList = await getDocumentList();
+            res.json(docList);
+        } catch (error) {
+            console.error('获取文档列表失败:', error);
+            res.status(500).json({ error: '获取文档列表失败', details: error.message });
+        }
+    });
+    
+    // 获取单个文档内容
+    app.get('/api/documentation/:id', requireLogin, async (req, res) => {
+        const docId = req.params.id;
+        console.log(`获取文档内容请求,ID: ${docId}`);
+        
+        try {
+            // 获取文档列表
+            const docList = await getDocumentList();
+            
+            // 查找指定ID的文档
+            const doc = docList.find(doc => doc.id === docId || doc._id === docId);
+            
+            if (!doc) {
+                return res.status(404).json({ error: '文档不存在', docId });
+            }
+            
+            // 如果文档未发布且用户不是管理员,则拒绝访问
+            if (!doc.published && !isAdmin(req.user)) {
+                return res.status(403).json({ error: '无权访问未发布的文档' });
+            }
+            
+            // 获取文档完整内容
+            const docContent = await getDocumentContent(docId);
+            
+            // 合并文档信息和内容
+            const fullDoc = {
+                ...doc,
+                content: docContent
+            };
+            
+            res.json(fullDoc);
+        } catch (error) {
+            console.error(`获取文档内容失败,ID: ${docId}`, error);
+            res.status(500).json({ 
+                error: '获取文档内容失败', 
+                details: error.message,
+                docId
+            });
+        }
+    });
+// ... existing code ... 

+ 396 - 0
hubcmdui/web/css/admin.css

@@ -0,0 +1,396 @@
+/* 今天登录的高亮样式 */
+.today-login {
+    background-color: #e6f7ff;
+    color: #1890ff;
+    padding: 2px 8px;
+    border-radius: 4px;
+    font-weight: 500;
+    display: inline-block;
+}
+
+/* 用户信息显示优化 */
+.account-info-item {
+    margin-bottom: 15px;
+    display: flex;
+    align-items: center;
+}
+
+.account-info-item .label {
+    font-weight: 500;
+    width: 120px;
+    flex-shrink: 0;
+    color: #555;
+}
+
+.account-info-item .value {
+    color: #1f2937;
+    font-weight: 400;
+}
+
+/* 加载中占位符样式 */
+.loading-placeholder {
+    display: inline-block;
+    width: 80px;
+    height: 14px;
+    background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
+    background-size: 200% 100%;
+    animation: loading 1.5s infinite;
+    border-radius: 4px;
+}
+
+@keyframes loading {
+    0% {
+        background-position: 200% 0;
+    }
+    100% {
+        background-position: -200% 0;
+    }
+}
+
+/* 菜单编辑弹窗样式 */
+.menu-edit-popup {
+    border-radius: 12px;
+    padding: 24px;
+    max-width: 500px;
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+}
+
+.menu-edit-title {
+    font-size: 1.5em;
+    color: #1f2937;
+    margin-bottom: 24px;
+    padding-bottom: 16px;
+    border-bottom: 2px solid #f3f4f6;
+    font-weight: 600;
+}
+
+.menu-edit-container {
+    text-align: left;
+}
+
+.menu-edit-container .form-group {
+    margin-bottom: 24px;
+}
+
+.menu-edit-container label {
+    display: block;
+    margin-bottom: 8px;
+    color: #4b5563;
+    font-weight: 500;
+    font-size: 0.95em;
+}
+
+.menu-edit-container .swal2-input,
+.menu-edit-container .swal2-select {
+    width: 100%;
+    padding: 10px 14px;
+    border: 2px solid #e5e7eb;
+    border-radius: 8px;
+    font-size: 14px;
+    transition: all 0.3s ease;
+    background-color: #f9fafb;
+}
+
+.menu-edit-container .swal2-input:hover,
+.menu-edit-container .swal2-select:hover {
+    border-color: #d1d5db;
+    background-color: #ffffff;
+}
+
+.menu-edit-container .swal2-input:focus,
+.menu-edit-container .swal2-select:focus {
+    border-color: #4CAF50;
+    outline: none;
+    box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.1);
+    background-color: #ffffff;
+}
+
+.menu-edit-confirm {
+    background-color: #4CAF50 !important;
+    padding: 10px 24px !important;
+    border-radius: 8px !important;
+    font-weight: 500 !important;
+    transition: all 0.3s ease !important;
+    font-size: 14px !important;
+    display: inline-flex !important;
+    align-items: center !important;
+    gap: 8px !important;
+}
+
+.menu-edit-confirm:hover {
+    background-color: #45a049 !important;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(76, 175, 80, 0.2);
+}
+
+.menu-edit-cancel {
+    background-color: #f3f4f6 !important;
+    color: #4b5563 !important;
+    padding: 10px 24px !important;
+    border-radius: 8px !important;
+    font-weight: 500 !important;
+    transition: all 0.3s ease !important;
+    font-size: 14px !important;
+    display: inline-flex !important;
+    align-items: center !important;
+    gap: 8px !important;
+}
+
+.menu-edit-cancel:hover {
+    background-color: #e5e7eb !important;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+/* 操作按钮样式优化 */
+.action-buttons {
+    display: flex;
+    gap: 8px;
+    justify-content: flex-end;
+}
+
+.action-btn {
+    width: 32px;
+    height: 32px;
+    border-radius: 6px;
+    border: none;
+    background-color: #f3f4f6;
+    color: #4b5563;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.2s ease;
+}
+
+.action-btn:hover {
+    background-color: #e5e7eb;
+    transform: translateY(-1px);
+}
+
+.action-btn i {
+    font-size: 14px;
+}
+
+.action-btn.edit-btn {
+    color: #3b82f6;
+}
+
+.action-btn.edit-btn:hover {
+    background-color: #dbeafe;
+}
+
+.action-btn.delete-btn {
+    color: #ef4444;
+}
+
+.action-btn.delete-btn:hover {
+    background-color: #fee2e2;
+}
+
+.action-btn.log-btn {
+    color: #10b981;
+}
+
+.action-btn.log-btn:hover {
+    background-color: #d1fae5;
+}
+
+.action-btn.start-btn {
+    color: #10b981;
+}
+
+.action-btn.start-btn:hover {
+    background-color: #d1fae5;
+}
+
+.action-btn.stop-btn {
+    color: #ef4444;
+}
+
+.action-btn.stop-btn:hover {
+    background-color: #fee2e2;
+}
+
+.action-btn.restart-btn {
+    color: #f59e0b;
+}
+
+.action-btn.restart-btn:hover {
+    background-color: #fef3c7;
+}
+
+/* Docker 状态指示器样式 */
+.status-indicator {
+    display: inline-flex;
+    align-items: center;
+    padding: 6px 12px;
+    border-radius: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    transition: all 0.3s;
+}
+
+.status-indicator.running {
+    background-color: rgba(76, 175, 80, 0.1);
+    color: #4CAF50;
+}
+
+.status-indicator.stopped {
+    background-color: rgba(244, 67, 54, 0.1);
+    color: #F44336;
+}
+
+.status-indicator i {
+    margin-right: 6px;
+    font-size: 16px;
+}
+
+/* 文档操作按钮样式优化 */
+.view-btn {
+    background-color: #f0f9ff !important;
+    color: #0ea5e9 !important;
+}
+
+.view-btn:hover {
+    background-color: #e0f2fe !important;
+    color: #0284c7 !important;
+}
+
+.edit-btn {
+    background-color: #f0fdf4 !important;
+    color: #22c55e !important;
+}
+
+.edit-btn:hover {
+    background-color: #dcfce7 !important;
+    color: #16a34a !important;
+}
+
+.delete-btn {
+    background-color: #fef2f2 !important;
+    color: #ef4444 !important;
+}
+
+.delete-btn:hover {
+    background-color: #fee2e2 !important;
+    color: #dc2626 !important;
+}
+
+.publish-btn {
+    background-color: #f0fdfa !important;
+    color: #14b8a6 !important;
+}
+
+.publish-btn:hover {
+    background-color: #ccfbf1 !important;
+    color: #0d9488 !important;
+}
+
+.unpublish-btn {
+    background-color: #fffbeb !important;
+    color: #f59e0b !important;
+}
+
+.unpublish-btn:hover {
+    background-color: #fef3c7 !important;
+    color: #d97706 !important;
+}
+
+/* 刷新按钮交互反馈 */
+.refresh-btn {
+    background-color: #f9fafb;
+    color: #4b5563;
+    border: 1px solid #e5e7eb;
+    padding: 6px 12px;
+    border-radius: 4px;
+    display: inline-flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 14px;
+    cursor: pointer;
+    transition: all 0.2s ease;
+}
+
+.refresh-btn:hover {
+    background-color: #f3f4f6;
+    color: #374151;
+}
+
+.refresh-btn.loading {
+    pointer-events: none;
+    opacity: 0.7;
+}
+
+.refresh-btn.loading i {
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* Docker未运行友好提示 */
+.docker-offline-container {
+    background-color: #f9fafb;
+    border: 1px solid #e5e7eb;
+    border-radius: 8px;
+    padding: 24px;
+    margin: 20px 0;
+    text-align: center;
+}
+
+.docker-offline-icon {
+    font-size: 40px;
+    color: #9ca3af;
+    margin-bottom: 16px;
+}
+
+.docker-offline-title {
+    font-size: 20px;
+    font-weight: 600;
+    color: #4b5563;
+    margin-bottom: 8px;
+}
+
+.docker-offline-message {
+    color: #6b7280;
+    margin-bottom: 20px;
+}
+
+.docker-offline-actions {
+    display: flex;
+    justify-content: center;
+    gap: 12px;
+}
+
+.docker-offline-btn {
+    padding: 8px 16px;
+    border-radius: 4px;
+    border: none;
+    font-weight: 500;
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    gap: 8px;
+    transition: all 0.2s ease;
+}
+
+.docker-offline-btn.primary {
+    background-color: #4f46e5;
+    color: white;
+}
+
+.docker-offline-btn.primary:hover {
+    background-color: #4338ca;
+}
+
+.docker-offline-btn.secondary {
+    background-color: #f3f4f6;
+    color: #4b5563;
+}
+
+.docker-offline-btn.secondary:hover {
+    background-color: #e5e7eb;
+}

+ 662 - 0
hubcmdui/web/css/custom.css

@@ -0,0 +1,662 @@
+/**
+ * 自定义样式表 - 增强UI交互和视觉效果
+ */
+
+/* 仪表盘错误通知 */
+.dashboard-error-notice {
+    background-color: #fee;
+    border-left: 4px solid #f44336;
+    padding: 12px 15px;
+    margin-bottom: 20px;
+    border-radius: 4px;
+    display: flex;
+    align-items: center;
+    animation: slideIn 0.4s ease-out;
+    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+}
+
+.dashboard-error-notice.fade-out {
+    animation: fadeOut 0.5s forwards;
+}
+
+.dashboard-error-notice i {
+    color: #f44336;
+    font-size: 20px;
+    margin-right: 10px;
+}
+
+.dashboard-error-notice span {
+    flex: 1;
+    color: #333;
+}
+
+.dashboard-error-notice button {
+    background-color: #f44336;
+    color: white;
+    border: none;
+    padding: 5px 10px;
+    border-radius: 3px;
+    cursor: pointer;
+    font-size: 14px;
+    transition: background-color 0.3s;
+}
+
+.dashboard-error-notice button:hover {
+    background-color: #d32f2f;
+}
+
+/* Docker状态指示器样式增强 */
+#dockerStatusIndicator {
+    display: flex;
+    align-items: center;
+    padding: 8px 12px;
+    border-radius: 20px;
+    font-weight: 500;
+}
+
+.docker-help-btn {
+    background: none;
+    border: none;
+    color: white;
+    margin-left: 8px;
+    cursor: pointer;
+    font-size: 16px;
+}
+
+/* Docker错误显示样式 */
+.docker-error-container {
+    padding: 30px 20px;
+    text-align: center;
+}
+
+.docker-error {
+    background-color: #fff6f6;
+    border-radius: 8px;
+    padding: 25px;
+    max-width: 600px;
+    margin: 0 auto;
+    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+    border: 1px solid #ffcdd2;
+}
+
+.docker-error i {
+    color: #f44336;
+    font-size: 48px;
+    margin-bottom: 15px;
+}
+
+.docker-error h3 {
+    color: #d32f2f;
+    margin-bottom: 15px;
+    font-size: 24px;
+}
+
+.docker-error p {
+    color: #555;
+    margin-bottom: 20px;
+    font-size: 16px;
+    line-height: 1.6;
+}
+
+.docker-error .retry-btn,
+.docker-error .help-btn {
+    padding: 10px 20px;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+    font-weight: 500;
+    margin: 5px;
+    transition: all 0.3s;
+}
+
+.docker-error .retry-btn {
+    background-color: #3d7cf4;
+    color: white;
+}
+
+.docker-error .help-btn {
+    background-color: #f5f5f5;
+    color: #333;
+}
+
+.docker-error .retry-btn:hover {
+    background-color: #2962ff;
+}
+
+.docker-error .help-btn:hover {
+    background-color: #e0e0e0;
+}
+
+/* 容器状态徽章 */
+.status-badge {
+    display: inline-block;
+    padding: 6px 12px;
+    border-radius: 20px;
+    font-size: 13px;
+    font-weight: 500;
+    text-align: center;
+}
+
+.status-running {
+    background-color: rgba(46, 204, 64, 0.15);
+    color: #2ecc40;
+    border: 1px solid rgba(46, 204, 64, 0.3);
+}
+
+.status-stopped,
+.status-exited {
+    background-color: rgba(255, 65, 54, 0.15);
+    color: #ff4136;
+    border: 1px solid rgba(255, 65, 54, 0.3);
+}
+
+.status-paused {
+    background-color: rgba(255, 133, 27, 0.15);
+    color: #ff851b;
+    border: 1px solid rgba(255, 133, 27, 0.3);
+}
+
+.status-created {
+    background-color: rgba(0, 116, 217, 0.15);
+    color: #0074d9;
+    border: 1px solid rgba(0, 116, 217, 0.3);
+}
+
+.status-unknown {
+    background-color: rgba(170, 170, 170, 0.15);
+    color: #aaaaaa;
+    border: 1px solid rgba(170, 170, 170, 0.3);
+}
+
+/* 操作按钮样式 */
+.action-buttons {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 5px;
+}
+
+.action-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    border-radius: 4px;
+    border: none;
+    cursor: pointer;
+    transition: all 0.2s;
+}
+
+.action-btn i {
+    font-size: 14px;
+}
+
+.log-btn {
+    background-color: #3d7cf4;
+    color: white;
+}
+
+.start-btn,
+.unpause-btn {
+    background-color: #2ecc40;
+    color: white;
+}
+
+.stop-btn {
+    background-color: #ff4136;
+    color: white;
+}
+
+.restart-btn {
+    background-color: #ff851b;
+    color: white;
+}
+
+.delete-btn {
+    background-color: #85144b;
+    color: white;
+}
+
+.action-btn:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+}
+
+.action-btn:disabled {
+    background-color: #ccc;
+    cursor: not-allowed;
+    opacity: 0.6;
+    transform: none;
+    box-shadow: none;
+}
+
+/* 加载动画 */
+.loading-animation {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 40px 0;
+}
+
+.spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid rgba(61, 124, 244, 0.1);
+    border-radius: 50%;
+    border-top: 4px solid #3d7cf4;
+    animation: spin 1s linear infinite;
+    margin-bottom: 15px;
+}
+
+.loading-spinner-small {
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 2px solid rgba(61, 124, 244, 0.1);
+    border-radius: 50%;
+    border-top: 2px solid #3d7cf4;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+@keyframes slideIn {
+    0% { transform: translateY(-20px); opacity: 0; }
+    100% { transform: translateY(0); opacity: 1; }
+}
+
+@keyframes fadeOut {
+    0% { opacity: 1; }
+    100% { opacity: 0; transform: translateY(-20px); }
+}
+
+/* 无容器状态样式 */
+.no-containers {
+    padding: 30px 0;
+    text-align: center;
+    color: #777;
+}
+
+.no-containers i {
+    font-size: 32px;
+    color: #aaa;
+    margin-bottom: 10px;
+}
+
+.no-containers p {
+    font-size: 18px;
+    margin-bottom: 5px;
+}
+
+.no-containers small {
+    font-size: 14px;
+    color: #999;
+}
+
+/* Docker故障排除指南样式 */
+.troubleshooting-guide {
+    text-align: left;
+}
+
+.troubleshooting-guide h4 {
+    margin: 15px 0 10px;
+    color: #333;
+    font-size: 16px;
+}
+
+.troubleshooting-guide ol {
+    padding-left: 20px;
+}
+
+.troubleshooting-guide li {
+    margin-bottom: 15px;
+}
+
+.troubleshooting-guide strong {
+    color: #3d7cf4;
+}
+
+.troubleshooting-guide .solution {
+    background-color: #f8f8f8;
+    padding: 8px 12px;
+    margin-top: 5px;
+    border-radius: 4px;
+    font-size: 14px;
+}
+
+.troubleshooting-guide code {
+    background-color: #e8e8e8;
+    padding: 2px 5px;
+    border-radius: 3px;
+    font-family: monospace;
+}
+
+.check-command,
+.docker-logs {
+    margin-top: 20px;
+    background-color: #f3f7ff;
+    padding: 10px 15px;
+    border-radius: 5px;
+    border-left: 3px solid #3d7cf4;
+}
+
+/* 菜单管理美化 */
+#menuTable {
+    border-collapse: separate;
+    border-spacing: 0;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+    margin-bottom: 30px;
+    width: 100%;
+}
+
+#menuTable th {
+    background-color: #3d7cf4;
+    color: white;
+    padding: 15px;
+    font-weight: 500;
+    text-align: left;
+    font-size: 15px;
+    border: none;
+}
+
+#menuTable tr:nth-child(even) {
+    background-color: rgba(61, 124, 244, 0.05);
+}
+
+#menuTable td {
+    padding: 12px 15px;
+    border-top: 1px solid #e9edf5;
+    vertical-align: middle;
+    font-size: 14px;
+}
+
+.new-item-row td {
+    background-color: #f2f7ff !important;
+    border-bottom: 2px solid #3d7cf4 !important;
+}
+
+#menuTable input[type="text"],
+#menuTable select {
+    width: 100%;
+    padding: 8px 10px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    font-size: 14px;
+    background-color: white;
+}
+
+#menuTable input[type="text"]:focus,
+#menuTable select:focus {
+    border-color: #3d7cf4;
+    box-shadow: 0 0 0 3px rgba(61, 124, 244, 0.2);
+    outline: none;
+}
+
+/* 文档管理增强 */
+#documentTable {
+    border-collapse: separate;
+    border-spacing: 0;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 4px 12px rgba(0,0,0,0.08);
+    margin: 20px 0 30px;
+    width: 100%;
+}
+
+#documentTable th {
+    background-color: #3d7cf4;
+    color: white;
+    padding: 15px;
+    font-weight: 500;
+    text-align: left;
+    font-size: 15px;
+    border: none;
+}
+
+#documentTable td {
+    padding: 12px 15px;
+    border-top: 1px solid #e9edf5;
+    vertical-align: middle;
+    font-size: 14px;
+}
+
+/* 文档编辑器增强 */
+#editorContainer {
+    border: 1px solid #ddd;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 4px 15px rgba(0,0,0,0.08);
+    margin-top: 30px;
+    display: none;
+}
+
+#documentTitle {
+    font-size: 18px;
+    font-weight: 500;
+    padding: 15px 20px;
+    border: none;
+    border-bottom: 1px solid #eee;
+    width: 100%;
+    box-shadow: none;
+    margin: 0;
+}
+
+#documentTitle:focus {
+    outline: none;
+    border-bottom-color: #3d7cf4;
+}
+
+.editormd-fullscreen {
+    z-index: 1000;
+}
+
+.editor-toolbar {
+    border-top: none !important;
+}
+
+.editor-preview-side {
+    border-left: 1px solid #ddd !important;
+}
+
+.editor-statusbar {
+    border-top: 1px solid #ddd !important;
+    padding: 8px 15px !important;
+}
+
+.editor-actions {
+    padding: 15px;
+    border-top: 1px solid #eee;
+    background-color: #f9f9f9;
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+}
+
+/* 文本输入框和表单样式增强 */
+.form-group {
+    margin-bottom: 20px;
+}
+
+.form-group label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: 500;
+    color: #333;
+}
+
+.form-control {
+    width: 100%;
+    padding: 12px 15px;
+    border: 1px solid #ddd;
+    border-radius: 6px;
+    font-size: 15px;
+    transition: all 0.3s;
+}
+
+.form-control:focus {
+    border-color: #3d7cf4;
+    box-shadow: 0 0 0 3px rgba(61, 124, 244, 0.2);
+    outline: none;
+}
+
+/* SweetAlert2自定义样式 */
+.swal2-popup {
+    padding: 25px !important;
+    border-radius: 10px !important;
+    width: auto !important;
+    min-width: 400px !important;
+}
+
+.swal2-title {
+    font-size: 24px !important;
+    font-weight: 600 !important;
+    color: #333 !important;
+}
+
+.swal2-content {
+    font-size: 16px !important;
+    color: #555 !important;
+}
+
+.swal2-input, .swal2-textarea, .swal2-select {
+    margin: 15px auto !important;
+}
+
+.swal2-styled.swal2-confirm {
+    background-color: #3d7cf4 !important;
+    padding: 12px 25px !important;
+    font-size: 16px !important;
+    border-radius: 6px !important;
+}
+
+/* 文档预览样式 */
+.document-preview-container {
+    z-index: 1100 !important;
+}
+
+.document-preview-popup {
+    max-width: 1000px !important;
+    overflow-y: hidden !important;
+}
+
+.document-preview-content {
+    padding: 0 !important;
+    margin: 0 !important;
+}
+
+.document-preview {
+    max-height: 70vh;
+    overflow-y: auto;
+    padding: 20px;
+    text-align: left;
+    background-color: white;
+    border-radius: 4px;
+    line-height: 1.6;
+}
+
+.document-preview h1 {
+    color: #2c3e50;
+    margin-top: 0;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #eee;
+}
+
+.document-preview h2 {
+    color: #3d7cf4;
+    padding-bottom: 5px;
+    border-bottom: 1px solid #eee;
+}
+
+.document-preview pre {
+    background-color: #f5f5f5;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+    padding: 10px;
+    overflow-x: auto;
+}
+
+.document-preview code {
+    background-color: #f5f5f5;
+    padding: 2px 4px;
+    border-radius: 3px;
+    font-family: "JetBrains Mono", monospace;
+}
+
+.document-preview img {
+    max-width: 100%;
+    height: auto;
+    display: block;
+    margin: 15px auto;
+    border-radius: 4px;
+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.document-preview table {
+    border-collapse: collapse;
+    width: 100%;
+    margin: 15px 0;
+}
+
+.document-preview table th,
+.document-preview table td {
+    border: 1px solid #ddd;
+    padding: 8px 12px;
+}
+
+.document-preview table th {
+    background-color: #f8f9fa;
+    font-weight: 600;
+}
+
+.document-preview blockquote {
+    border-left: 4px solid #3d7cf4;
+    padding: 10px 15px;
+    color: #555;
+    background-color: #f9f9f9;
+    margin: 15px 0;
+}
+
+/* 为文档添加打印样式 */
+@media print {
+    .document-preview {
+        height: auto;
+        overflow: visible;
+        background: white;
+        color: black;
+    }
+    
+    .document-preview * {
+        color: black !important;
+    }
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+    .action-buttons {
+        flex-direction: column;
+    }
+    
+    #documentTable th,
+    #documentTable td,
+    #menuTable th,
+    #menuTable td {
+        padding: 8px;
+        font-size: 13px;
+    }
+    
+    .status-badge {
+        padding: 4px 8px;
+        font-size: 12px;
+    }
+    
+    .swal2-popup {
+        min-width: 300px !important;
+        padding: 15px !important;
+    }
+}

+ 1 - 0
hubcmdui/web/data/documentation/index.json

@@ -0,0 +1 @@
+[]

BIN
hubcmdui/web/images/login-bg.jpg


+ 372 - 191
hubcmdui/web/index.html

@@ -206,7 +206,135 @@
             }
         }
         
-        let proxyDomain = ''; // 默认代理加速地址
+        // ========================================
+        // === 文档加载相关函数 (移到此处) ===
+        // ========================================
+        let documentationLoaded = false;
+        async function loadAndDisplayDocumentation() {
+            // 防止重复加载
+            if (documentationLoaded) {
+                console.log('文档已加载,跳过重复加载');
+                return;
+            }
+            
+            const docListContainer = document.getElementById('documentList');
+            const docContentContainer = document.getElementById('documentationText');
+
+            if (!docListContainer || !docContentContainer) {
+                console.warn('找不到文档列表或内容容器,可能不是文档页面');
+                return; // 如果容器不存在,则不执行加载
+            }
+
+            try {
+                console.log('开始加载文档列表和内容...');
+                
+                // 显示加载状态
+                docListContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档列表...</div>';
+                docContentContainer.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 请从左侧选择文档...</div>';
+                
+                // 获取文档列表
+                const response = await fetch('/api/documentation');
+                if (!response.ok) {
+                    throw new Error(`获取文档列表失败: ${response.status}`);
+                }
+                
+                const data = await response.json();
+                console.log('获取到文档列表:', data);
+                
+                // 保存到全局变量
+                window.documentationData = data;
+                documentationLoaded = true; // 标记为已加载
+                
+                if (!Array.isArray(data) || data.length === 0) {
+                    docListContainer.innerHTML = `
+                        <h2>文档目录</h2>
+                        <div class="empty-list">
+                            <i class="fas fa-file-alt fa-3x"></i>
+                            <p>暂无文档</p>
+                        </div>
+                    `;
+                    docContentContainer.innerHTML = `
+                        <div class="empty-content">
+                            <i class="fas fa-file-alt fa-3x"></i>
+                            <h2>暂无文档</h2>
+                            <p>系统中还没有添加任何使用教程文档。</p>
+                        </div>
+                    `;
+                    return;
+                }
+                
+                // 创建文档列表
+                let html = '<h2>文档目录</h2><ul class="doc-list">';
+                data.forEach((doc, index) => {
+                    // 确保doc有效
+                    if (doc && doc.id && doc.title) {
+                         html += `
+                            <li class="doc-item" data-id="${doc.id}">
+                                <a href="javascript:void(0)" onclick="showDocument(${index})">
+                                    <i class="fas fa-file-alt"></i>
+                                    <span>${doc.title}</span>
+                                </a>
+                            </li>
+                        `;
+                    } else {
+                        console.warn('发现无效的文档数据:', doc);
+                    }
+                });
+                html += '</ul>';
+                
+                docListContainer.innerHTML = html;
+                
+                // 默认加载第一篇文档
+                if (data.length > 0 && data[0]) {
+                    showDocument(0);
+                     // 激活第一个列表项
+                    const firstLink = docListContainer.querySelector('.doc-item a');
+                    if (firstLink) {
+                        firstLink.classList.add('active');
+                    }
+                } else {
+                    // 如果第一个文档无效,显示空状态
+                     docContentContainer.innerHTML = `
+                        <div class="empty-content">
+                            <i class="fas fa-file-alt fa-3x"></i>
+                            <p>请从左侧选择一篇文档查看</p>
+                        </div>
+                    `;
+                }
+            } catch (error) {
+                console.error('加载文档列表失败:', error);
+                documentationLoaded = false; // 加载失败,允许重试
+                
+                if (docListContainer) {
+                    docListContainer.innerHTML = `
+                        <h2>文档目录</h2>
+                        <div class="error-item">
+                            <i class="fas fa-exclamation-triangle"></i>
+                            <p>${error.message}</p>
+                            <button class="btn btn-sm btn-primary mt-2" onclick="loadAndDisplayDocumentation()">重试</button>
+                        </div>
+                    `;
+                }
+                
+                if (docContentContainer) {
+                    docContentContainer.innerHTML = `
+                        <div class="error-container">
+                            <i class="fas fa-exclamation-triangle fa-3x"></i>
+                            <h2>加载失败</h2>
+                            <p>无法获取文档列表: ${error.message}</p>
+                        </div>
+                    `;
+                }
+            }
+        }
+        // ========================================
+        // === 文档加载相关函数结束 ===
+        // ========================================
+
+        // ========================================
+        // === 全局变量和状态 ===
+        // ========================================
+        let proxyDomain = ''; 
         let currentIndex = 0;
         let items = [];
         let currentPage = 1;
@@ -215,6 +343,43 @@
         let currentTagPage = 1;
         let currentImageData = null;
 
+        // ========================================
+        // === 新增:全局提示函数 ===
+        // ========================================
+        function showToastNotification(message, type = 'info') { // types: info, success, error
+            // 移除任何现有的通知
+            const existingNotification = document.querySelector('.toast-notification');
+            if (existingNotification) {
+                existingNotification.remove();
+            }
+
+            // 创建新的通知元素
+            const toast = document.createElement('div');
+            toast.className = `toast-notification ${type}`;
+            
+            // 设置图标和内容
+            let iconClass = 'fas fa-info-circle';
+            if (type === 'success') iconClass = 'fas fa-check-circle';
+            if (type === 'error') iconClass = 'fas fa-exclamation-circle';
+            
+            toast.innerHTML = `<i class="${iconClass}"></i> ${message}`;
+            
+            document.body.appendChild(toast);
+            
+            // 动画效果 (如果需要的话,可以在CSS中定义 @keyframes fadeIn)
+            // toast.style.animation = 'fadeIn 0.3s ease-in';
+
+            // 设定时间后自动移除
+            setTimeout(() => {
+                toast.style.opacity = '0'; // 开始淡出
+                toast.style.transition = 'opacity 0.3s ease-out';
+                setTimeout(() => toast.remove(), 300); // 淡出后移除DOM
+            }, 3500); // 显示 3.5 秒
+        }
+        
+        // ========================================
+        // === 其他函数定义 ===
+        // ========================================
         // 标签切换功能
         function switchTab(tabName) {
             const tabs = document.querySelectorAll('.tab');
@@ -252,7 +417,7 @@
             document.getElementById('paginationContainer').style.display = 'none';
 
             if (tabName === 'documentation') {
-                fetchDocumentation();
+                loadAndDisplayDocumentation();
             } else if (tabName === 'accelerate') {
                 // 重置显示状态
                 document.querySelector('.quick-guide').style.display = 'block';
@@ -414,7 +579,7 @@
         async function searchDockerHub(page = 1) {
             const searchTerm = document.getElementById('searchInput').value.trim();
             if (!searchTerm) {
-                showToast('请输入搜索关键词');
+                showToastNotification('请输入搜索关键词', 'info');
                 return;
             }
             // 如果搜索词改变,重置为第1页
@@ -436,6 +601,8 @@
             document.getElementById('imageTagsView').style.display = 'none';
 
             try {
+                console.log(`搜索Docker Hub: 关键词=${searchTerm}, 页码=${page}`);
+                
                 // 使用新的fetchWithRetry函数
                 const data = await fetchWithRetry(
                     `/api/dockerhub/search?term=${encodeURIComponent(searchTerm)}&page=${page}`
@@ -608,9 +775,6 @@
                     <div class="tag-actions">
                         <div class="tag-search-container">
                             <input type="text" id="tagSearchInput" placeholder="搜索TAG..." onkeyup="filterTags()">
-                            <button class="search-btn" onclick="filterTags()">
-                                <i class="fas fa-search"></i> 搜索
-                            </button>
                         </div>
                         <button id="loadAllTagsBtn" class="load-all-btn" onclick="loadAllTags()" ${loadAllBtnDisabled ? 'disabled' : ''}>
                             <i class="fas fa-cloud-download-alt"></i> 加载全部TAG
@@ -652,25 +816,10 @@
                     </div>
                 `;
                 
-                // 显示通知
-                showToast(`<i class="fas fa-exclamation-circle"></i> 加载镜像详情失败: ${error.message}`, true);
+                showToastNotification(`加载镜像详情失败: ${error.message}`, 'error');
             }
         }
 
-        // 添加formatNumber函数定义
-        function formatNumber(num) {
-            if (num >= 1000000000) {
-                return (num >= 1500000000 ? '1B+' : '1B');
-            } else if (num >= 1000000) {
-                const m = Math.floor(num / 1000000);
-                return (m >= 100 ? '100M+' : m + 'M');
-            } else if (num >= 1000) {
-                const k = Math.floor(num / 1000);
-                return (k >= 100 ? '100K+' : k + 'K');
-            }
-            return num.toString();
-        }
-
         // 新增: 加载所有标签 - 改进错误处理
         async function loadAllTags() {
             if (!currentImageData) {
@@ -695,7 +844,7 @@
                 
                 if (totalTags === 0) {
                     tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
-                    showToast(`<i class="fas fa-info-circle"></i> 该镜像没有可用的标签`, false);
+                    showToastNotification(`该镜像没有可用的标签`, 'info');
                     loadAllTagsBtn.disabled = false;
                     loadAllTagsBtn.innerHTML = '<i class="fas fa-cloud-download-alt"></i> 加载全部TAG';
                     return;
@@ -767,8 +916,7 @@
                     // 显示第一页标签(这会自动创建分页控制器)
                     displayAllTagsPage(1);
                     
-                    // 显示提示
-                    showToast(`<i class="fas fa-check-circle"></i> 成功加载 ${allTags.length} / ${totalTags} 个标签,分${clientTotalPages}页显示`, false);
+                    showToastNotification(`成功加载 ${allTags.length} / ${totalTags} 个标签,分${clientTotalPages}页显示`, 'success');
                     
                     // 滚动到顶部
                     window.scrollTo({
@@ -778,7 +926,7 @@
                     
                 } else {
                     tagsResults.innerHTML = '<div class="message-container"><i class="fas fa-info-circle"></i><p>未找到标签信息</p></div>';
-                    showToast(`<i class="fas fa-info-circle"></i> 未能加载标签`, true);
+                    showToastNotification(`未能加载标签`, 'info');
                 }
                 
             } catch (error) {
@@ -792,7 +940,7 @@
                         </button>
                     </div>
                 `;
-                showToast(`<i class="fas fa-exclamation-circle"></i> 加载全部标签失败: ${error.message}`, true);
+                showToastNotification(`加载全部标签失败: ${error.message}`, 'error');
             } finally {
                 // 恢复按钮状态
                 loadAllTagsBtn.disabled = false;
@@ -859,8 +1007,7 @@
                 `;
                 document.getElementById('tagPaginationContainer').style.display = 'none';
                 
-                // 显示通知
-                showToast(`<i class="fas fa-exclamation-circle"></i> 加载标签失败: ${error.message}`, true);
+                showToastNotification(`加载标签失败: ${error.message}`, 'error');
             }
         }
 
@@ -1300,189 +1447,223 @@
             }
         }
 
-        // 获取文档列表
-        async function fetchDocumentation() {
-            try {
-                const response = await fetch('/api/documentation');
-                if (!response.ok) {
-                    throw new Error(`HTTP error! status: ${response.status}`);
-                }
-                const documents = await response.json();
-                console.log('Fetched documents:', documents);
-                const documentList = document.getElementById('documentList');
-                const documentationText = document.getElementById('documentationText');
+        // 显示指定的文档
+        function showDocument(index) {
+            console.log('显示文档索引:', index);
+            
+            if (!window.documentationData || !Array.isArray(window.documentationData)) {
+                console.error('文档数据不可用');
+                return;
+            }
+            
+            // 处理数字索引或字符串ID
+            let docIndex = index;
+            let doc = null;
+            
+            if (typeof index === 'string') {
+                // 如果是ID,找到对应的索引
+                docIndex = window.documentationData.findIndex(doc => 
+                    (doc.id === index || doc._id === index)
+                );
                 
-                if (Array.isArray(documents) && documents.length > 0) {
-                    documentList.innerHTML = '<h2>文档列表</h2>';
-                    const ul = document.createElement('ul');
-                    documents.forEach(doc => {
-                        if (doc && doc.id && doc.title) {
-                            const li = document.createElement('li');
-                            const link = document.createElement('a');
-                            link.href = 'javascript:void(0);';
-                            link.textContent = doc.title;
-                            link.onclick = () => {
-                                // 移除其他链接的active类
-                                document.querySelectorAll('#documentList a').forEach(a => a.classList.remove('active'));
-                                // 添加当前链接的active类
-                                link.classList.add('active');
-                                showDocument(doc.id);
-                            };
-                            li.appendChild(link);
-                            ul.appendChild(li);
-                        }
-                    });
-                    documentList.appendChild(ul);
-                    
-                    // 默认选中第一个文档
-                    if (documents[0] && documents[0].id) {
-                        const firstLink = ul.querySelector('a');
-                        if (firstLink) {
-                            firstLink.classList.add('active');
-                            showDocument(documents[0].id);
-                        }
-                    }
-                } else {
-                    documentationText.innerHTML = '暂无文档内容';
+                if (docIndex === -1) {
+                    console.error('找不到ID为', index, '的文档');
+                    return;
                 }
-            } catch (error) {
-                console.error('获取文档列表失败:', error);
-                document.getElementById('documentationText').innerHTML = '加载文档列表失败,请稍后再试。错误详情: ' + error.message;
             }
-        }
-
-        async function showDocument(id) {
-            try {
-                console.log('Attempting to show document with id:', id);
-                const response = await fetch(`/api/documentation/${id}`);
-                if (!response.ok) {
-                    throw new Error(`HTTP error! status: ${response.status}`);
+            
+            doc = window.documentationData[docIndex];
+            
+            if (!doc) {
+                console.error('指定索引的文档不存在:', docIndex);
+                return;
+            }
+            
+            console.log('文档数据:', doc);
+            
+            // 高亮选中的文档
+            const docLinks = document.querySelectorAll('.doc-list li a');
+            docLinks.forEach((link, i) => {
+                if (i === docIndex) {
+                    link.classList.add('active');
+                } else {
+                    link.classList.remove('active');
                 }
-                const documentData = await response.json();
-                console.log('Fetched document:', documentData);
-                const documentationText = document.getElementById('documentationText');
-                if (documentData && documentData.content && documentData.content.trim() !== '') {
-                    // 渲染文档内容
-                    documentationText.innerHTML = `<h2>${documentData.title || '无标题'}</h2>` + marked.parse(documentData.content);
-                    // 为所有代码块添加复制按钮和终端样式
-                    const codeBlocks = documentationText.querySelectorAll('pre code');
-                    codeBlocks.forEach((codeBlock) => {
-                        const pre = codeBlock.parentElement;
-                        const code = codeBlock.textContent;
+            });
+            
+            const docContent = document.getElementById('documentationText');
+            if (!docContent) {
+                console.error('找不到文档内容容器');
+                return;
+            }
+            
+            // 显示加载状态
+            docContent.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档内容...</div>';
+            
+            // 如果文档内容不存在,则需要获取完整内容
+            if (!doc.content) {
+                const docId = doc.id || doc._id;
+                console.log('获取文档内容,ID:', docId);
+                
+                fetch(`/api/documentation/${docId}`)
+                    .then(response => {
+                        console.log('文档API响应:', response.status, response.statusText);
+                        if (!response.ok) {
+                            throw new Error(`获取文档内容失败: ${response.status}`);
+                        }
+                        return response.json();
+                    })
+                    .then(fullDoc => {
+                        console.log('获取到完整文档:', fullDoc);
                         
-                        // 创建终端风格容器
-                        const wrapper = document.createElement('div');
-                        wrapper.className = 'command-terminal';
-                        wrapper.innerHTML = `
-                            <div class="terminal-header">
-                                <div class="terminal-button button-red"></div>
-                                <div class="terminal-button button-yellow"></div>
-                                <div class="terminal-button button-green"></div>
+                        // 更新缓存的文档内容
+                        window.documentationData[docIndex].content = fullDoc.content;
+                        
+                        // 渲染文档内容
+                        renderDocumentContent(docContent, fullDoc);
+                    })
+                    .catch(error => {
+                        console.error('获取文档内容失败:', error);
+                        docContent.innerHTML = `
+                            <div class="error-container">
+                                <i class="fas fa-exclamation-triangle fa-3x"></i>
+                                <h2>加载失败</h2>
+                                <p>无法获取文档内容: ${error.message}</p>
                             </div>
                         `;
-                        
-                        // 创建复制按钮
-                        const copyButton = document.createElement('button');
-                        copyButton.className = 'copy-btn';
-                        copyButton.textContent = '复制';
-                        copyButton.onclick = () => copyToClipboard(code, copyButton);
-                        
-                        // 正确的DOM操作顺序
-                        pre.parentNode.insertBefore(wrapper, pre);  // 1. 先将wrapper插入到pre前面
-                        wrapper.appendChild(pre);                   // 2. 将pre移动到wrapper内
-                        pre.appendChild(copyButton);                // 3. 将复制按钮添加到pre内
                     });
-                } else {
-                    documentationText.innerHTML = '该文档没有内容或格式不正确';
-                }
-            } catch (error) {
-                console.error('获取文档内容失败:', error);
-                document.getElementById('documentationText').innerHTML = '加载文档内容失败,请稍后再试。错误详情: ' + error.message;
+            } else {
+                // 直接渲染已有的文档内容
+                renderDocumentContent(docContent, doc);
             }
         }
+        
+        // 确保showDocument函数在全局范围内可用
+        window.showDocument = showDocument;
+        
+        // 渲染文档内容
+        function renderDocumentContent(container, doc) {
+            if (!container) return;
+            
+            console.log('正在渲染文档:', doc);
 
-        // 获取并加载配置
-        async function loadConfig() {
-            try {
-                const response = await fetch('/api/config');
-                const config = await response.json();
-                if (config.logo) {
-                    document.querySelector('.logo').src = config.logo;
-                }
-                if (config.menuItems && Array.isArray(config.menuItems)) {
-                    const navMenu = document.getElementById('navMenu');
-                    navMenu.innerHTML = ''; // 清空菜单
-                    config.menuItems.forEach(item => {
-                        const a = document.createElement('a');
-                        a.href = item.link;
-                        a.textContent = item.text;
-                        if (item.newTab) {
-                            a.target = '_blank';
-                        }
-                        navMenu.appendChild(a);
-                    });
-                }
-                if (config.proxyDomain) {
-                    proxyDomain = config.proxyDomain;
-                }
-                if (config.searchApiEndpoint) {
-                    window.searchApiEndpoint = config.searchApiEndpoint;
-                }
-            } catch (error) {
-                console.error('加载配置失败:', error);
+            // 确保有内容可渲染
+            if (!doc.content && !doc.path) {
+                container.innerHTML = `
+                    <h1>${doc.title || '未知文档'}</h1>
+                    <div class="empty-content">
+                        <i class="fas fa-file-alt fa-3x"></i>
+                        <p>该文档暂无内容</p>
+                    </div>
+                `;
+                return;
             }
-        }
 
-        function useImage(imageName) {
-            if (imageName) {
-                console.log("使用镜像:", imageName);
-                document.getElementById('imageInput').value = imageName;
-                switchTab('accelerate');
-                // 直接生成加速命令,无需用户再次点击
-                generateCommands(imageName);
+            // 根据文档内容类型进行渲染
+            if (doc.content) {
+                renderMarkdownContent(container, doc);
             } else {
-                alert('无效的 Docker 镜像名称');
+                // 如果是文件路径但无内容,尝试获取
+                fetch(`/api/documentation/file?path=${encodeURIComponent(doc.id + '.md')}`)
+                    .then(response => {
+                        console.log('文件内容响应:', response.status, response.statusText);
+                        if (!response.ok) {
+                            throw new Error(`获取文件内容失败: ${response.status}`);
+                        }
+                        return response.text();
+                    })
+                    .then(content => {
+                        console.log('获取到文件内容,长度:', content.length);
+                        doc.content = content;
+                        renderMarkdownContent(container, doc);
+                    })
+                    .catch(error => {
+                        console.error('获取文件内容失败:', error);
+                        container.innerHTML = `
+                            <div class="error-container">
+                                <i class="fas fa-exclamation-triangle fa-3x"></i>
+                                <h2>加载失败</h2>
+                                <p>无法获取文档内容: ${error.message}</p>
+                            </div>
+                        `;
+                    });
             }
         }
-
-        // 改进Toast通知功能,支持HTML内容
-        function showToast(message, isError = false) {
-            // 移除任何现有的提示
-            const existingToasts = document.querySelectorAll('.toast-notification');
-            existingToasts.forEach(toast => toast.remove());
+        
+        // 渲染Markdown内容
+        function renderMarkdownContent(container, doc) {
+            if (!container) return;
             
-            // 创建新的提示
-            const toast = document.createElement('div');
-            toast.className = `toast-notification ${isError ? 'error' : 'info'}`;
-            toast.innerHTML = message; // 使用innerHTML而不是textContent以支持HTML
-            document.body.appendChild(toast);
+            console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
             
-            // 3秒后自动消失
-            setTimeout(() => {
-                toast.classList.add('fade-out');
-                setTimeout(() => toast.remove(), 300);
-            }, 5000); // 延长显示时间到5秒,给用户更多时间阅读
+            if (doc.content) {
+                // 使用marked渲染Markdown内容
+                if (window.marked) {
+                    try {
+                        const parsedContent = marked.parse(doc.content);
+                        // 结构:内容(含标题)-> 元数据
+                        container.innerHTML = ` 
+                            <div class="doc-content">${parsedContent}</div>
+                            <div class="doc-meta">
+                                ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
+                            </div>
+                        `;
+                    } catch (error) {
+                        console.error('Markdown解析失败:', error);
+                        // 发生错误时,仍然显示原始Markdown内容 + Meta
+                        container.innerHTML = ` 
+                            <div class="doc-content">${doc.content}</div>
+                            <div class="doc-meta">
+                                ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
+                            </div>
+                        `;
+                    }
+                } else {
+                    // marked 不可用时,直接显示内容 + Meta
+                    container.innerHTML = ` 
+                        <div class="doc-content">${doc.content}</div>
+                        <div class="doc-meta">
+                            ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
+                        </div>
+                    `;
+                }
+            } else {
+                // 文档无内容时,显示占位符
+                container.innerHTML = ` 
+                     <div class="doc-content">
+                        <div class="empty-content">
+                            <i class="fas fa-file-alt fa-3x"></i>
+                            <p>该文档暂无内容</p>
+                        </div>
+                     </div>
+                     <div class="doc-meta">
+                         <span>文档信息不可用</span>
+                     </div>
+                `;
+            }
         }
-
-        loadConfig();
-
-        // 添加缺失的showSearchResults函数
-        function showSearchResults() {
-            // 显示搜索结果列表,隐藏标签视图
-            document.getElementById('searchResultsList').style.display = 'block';
-            document.getElementById('imageTagsView').style.display = 'none';
-            
-            // 清空标签搜索输入框
-            const tagSearchInput = document.getElementById('tagSearchInput');
-            if (tagSearchInput) {
-                tagSearchInput.value = '';
+        
+        // 加载菜单
+        loadMenu();
+        
+        // DOMContentLoaded 事件监听器
+        document.addEventListener('DOMContentLoaded', function() {
+            // 确保元素存在再添加事件监听器
+            const searchInput = document.getElementById('searchInput');
+            if (searchInput) {
+                searchInput.addEventListener('keypress', function(event) {
+                    if (event.key === 'Enter') {
+                        searchDockerHub(1);
+                    }
+                });
             }
             
-            // 显示分页控件(如果有搜索结果)
-            if (document.getElementById('searchResults').children.length > 0) {
-                document.getElementById('paginationContainer').style.display = 'flex';
-            }
-        }
+            // 加载菜单
+            loadMenu();
+            
+            // 统一调用文档加载函数
+            loadAndDisplayDocumentation();
+        });
     </script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
 </body>

+ 495 - 0
hubcmdui/web/js/app.js

@@ -0,0 +1,495 @@
+// 应用程序入口模块
+
+document.addEventListener('DOMContentLoaded', function() {
+    console.log('DOM 加载完成,初始化模块...');
+    
+    // 启动应用程序
+    core.initApp();
+    
+    // 在核心应用初始化后,再初始化其他模块
+    initializeModules(); 
+    console.log('模块初始化已启动');
+});
+
+// 初始化所有模块
+async function initializeModules() {
+    console.log('开始初始化所有模块...');
+    try {
+        // 初始化核心模块
+        console.log('正在初始化核心模块...');
+        if (typeof core !== 'undefined') {
+            // core.init() 已经在core.initApp()中调用,这里不再重复调用
+            console.log('核心模块初始化完成');
+        } else {
+            console.error('核心模块未定义');
+        }
+
+        // 初始化认证模块
+        console.log('正在初始化认证模块...');
+        if (typeof auth !== 'undefined') {
+            await auth.init();
+            console.log('认证模块初始化完成');
+        } else {
+            console.error('认证模块未定义');
+        }
+
+        // 初始化用户中心
+        console.log('正在初始化用户中心...');
+        if (typeof userCenter !== 'undefined') {
+            await userCenter.init();
+            console.log('用户中心初始化完成');
+        } else {
+            console.error('用户中心未定义');
+        }
+
+        // 初始化菜单管理
+        console.log('正在初始化菜单管理...');
+        if (typeof menuManager !== 'undefined') {
+            await menuManager.init();
+            console.log('菜单管理初始化完成');
+        } else {
+            console.error('菜单管理未定义');
+        }
+
+        // 初始化文档管理
+        console.log('正在初始化文档管理...');
+        if (typeof documentManager !== 'undefined') {
+            await documentManager.init();
+            console.log('文档管理初始化完成');
+        } else {
+            console.error('文档管理未定义');
+        }
+
+        // 初始化Docker管理
+        console.log('正在初始化Docker管理...');
+        if (typeof dockerManager !== 'undefined') {
+            await dockerManager.init();
+            console.log('Docker管理初始化完成');
+        } else {
+            console.error('Docker管理未定义');
+        }
+
+        // 初始化系统状态
+        console.log('正在初始化系统状态...');
+        if (typeof systemStatus !== 'undefined') {
+            if (typeof systemStatus.initDashboard === 'function') {
+                 await systemStatus.initDashboard();
+                 console.log('系统状态初始化完成');
+            } else {
+                 console.error('systemStatus.initDashboard 函数未定义!');
+            }
+        } else {
+            console.error('系统状态未定义');
+        }
+
+        // 初始化网络测试
+        console.log('正在初始化网络测试...');
+        if (typeof networkTest !== 'undefined') {
+            await networkTest.init();
+            console.log('网络测试初始化完成');
+        } else {
+            console.error('网络测试未定义');
+        }
+
+        // 加载监控配置
+        await loadMonitoringConfig();
+        
+        // 显示默认页面 - 使用core中的showSection函数
+        core.showSection('dashboard');
+        
+        console.log('所有模块初始化完成');
+    } catch (error) {
+        console.error('初始化模块时发生错误:', error);
+        // 尝试使用 core.showAlert,如果 core 本身加载失败则用 console.error
+        if (typeof core !== 'undefined' && core.showAlert) {
+             core.showAlert('初始化失败: ' + error.message, 'error');
+        } else {
+             console.error('核心模块无法加载,无法显示警告弹窗');
+        }
+    }
+}
+
+// 监控配置相关函数
+function loadMonitoringConfig() {
+    console.log('正在加载监控配置...');
+    fetch('/api/monitoring-config')
+        .then(response => {
+            console.log('监控配置API响应:', response.status, response.statusText);
+            if (!response.ok) {
+                throw new Error(`HTTP状态错误 ${response.status}: ${response.statusText}`);
+            }
+            return response.json();
+        })
+        .then(config => {
+            console.log('获取到监控配置:', config);
+            // 填充表单
+            document.getElementById('notificationType').value = config.notificationType || 'wechat';
+            document.getElementById('webhookUrl').value = config.webhookUrl || '';
+            document.getElementById('telegramToken').value = config.telegramToken || '';
+            document.getElementById('telegramChatId').value = config.telegramChatId || '';
+            document.getElementById('monitorInterval').value = config.monitorInterval || 60;
+            
+            // 显示或隐藏相应的字段
+            toggleNotificationFields();
+            
+            // 更新监控状态
+            document.getElementById('monitoringStatus').textContent = 
+                config.isEnabled ? '已启用' : '已禁用';
+            document.getElementById('monitoringStatus').style.color = 
+                config.isEnabled ? '#4CAF50' : '#F44336';
+            
+            document.getElementById('toggleMonitoringBtn').textContent = 
+                config.isEnabled ? '禁用监控' : '启用监控';
+            
+            console.log('监控配置加载完成');
+        })
+        .catch(error => {
+            console.error('加载监控配置失败:', error);
+            // 使用安全的方式调用core.showAlert
+            if (typeof core !== 'undefined' && core && typeof core.showAlert === 'function') {
+                core.showAlert('加载监控配置失败: ' + error.message, 'error');
+            } else {
+                // 如果core未定义,使用alert作为备选
+                alert('加载监控配置失败: ' + error.message);
+            }
+        });
+}
+
+function toggleNotificationFields() {
+    const type = document.getElementById('notificationType').value;
+    if (type === 'wechat') {
+        document.getElementById('wechatFields').style.display = 'block';
+        document.getElementById('telegramFields').style.display = 'none';
+    } else {
+        document.getElementById('wechatFields').style.display = 'none';
+        document.getElementById('telegramFields').style.display = 'block';
+    }
+}
+
+function testNotification() {
+    const notificationType = document.getElementById('notificationType').value;
+    const webhookUrl = document.getElementById('webhookUrl').value;
+    const telegramToken = document.getElementById('telegramToken').value;
+    const telegramChatId = document.getElementById('telegramChatId').value;
+    
+    // 验证输入
+    if (notificationType === 'wechat' && !webhookUrl) {
+        Swal.fire({
+            icon: 'error',
+            title: '验证失败',
+            text: '请输入企业微信机器人 Webhook URL',
+            confirmButtonText: '确定'
+        });
+        return;
+    }
+    
+    if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+        Swal.fire({
+            icon: 'error',
+            title: '验证失败',
+            text: '请输入 Telegram Bot Token 和 Chat ID',
+            confirmButtonText: '确定'
+        });
+        return;
+    }
+    
+    // 显示处理中的状态
+    Swal.fire({
+        title: '发送中...',
+        html: '<i class="fas fa-spinner fa-spin"></i> 正在发送测试通知',
+        showConfirmButton: false,
+        allowOutsideClick: false,
+        willOpen: () => {
+            Swal.showLoading();
+        }
+    });
+    
+    fetch('/api/test-notification', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            notificationType,
+            webhookUrl,
+            telegramToken,
+            telegramChatId
+        })
+    })
+    .then(response => {
+        if (!response.ok) throw new Error('测试通知失败');
+        return response.json();
+    })
+    .then(() => {
+        Swal.fire({
+            icon: 'success',
+            title: '发送成功',
+            text: '测试通知已发送,请检查您的接收设备',
+            timer: 2000,
+            showConfirmButton: false
+        });
+    })
+    .catch(error => {
+        console.error('测试通知失败:', error);
+        Swal.fire({
+            icon: 'error',
+            title: '发送失败',
+            text: '测试通知发送失败: ' + error.message,
+            confirmButtonText: '确定'
+        });
+    });
+}
+
+function saveMonitoringConfig() {
+    const notificationType = document.getElementById('notificationType').value;
+    const webhookUrl = document.getElementById('webhookUrl').value;
+    const telegramToken = document.getElementById('telegramToken').value;
+    const telegramChatId = document.getElementById('telegramChatId').value;
+    const monitorInterval = document.getElementById('monitorInterval').value;
+    
+    // 验证输入
+    if (notificationType === 'wechat' && !webhookUrl) {
+        Swal.fire({
+            icon: 'error',
+            title: '验证失败',
+            text: '请输入企业微信机器人 Webhook URL',
+            confirmButtonText: '确定'
+        });
+        return;
+    }
+    
+    if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
+        Swal.fire({
+            icon: 'error',
+            title: '验证失败',
+            text: '请输入 Telegram Bot Token 和 Chat ID',
+            confirmButtonText: '确定'
+        });
+        return;
+    }
+    
+    // 显示保存中的状态
+    Swal.fire({
+        title: '保存中...',
+        html: '<i class="fas fa-spinner fa-spin"></i> 正在保存监控配置',
+        showConfirmButton: false,
+        allowOutsideClick: false,
+        willOpen: () => {
+            Swal.showLoading();
+        }
+    });
+    
+    fetch('/api/monitoring-config', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify({
+            notificationType,
+            webhookUrl,
+            telegramToken,
+            telegramChatId,
+            monitorInterval,
+            isEnabled: document.getElementById('monitoringStatus').textContent === '已启用'
+        })
+    })
+    .then(response => {
+        if (!response.ok) throw new Error('保存配置失败');
+        return response.json();
+    })
+    .then(() => {
+        Swal.fire({
+            icon: 'success',
+            title: '保存成功',
+            text: '监控配置已成功保存',
+            timer: 2000,
+            showConfirmButton: false
+        });
+        loadMonitoringConfig();
+    })
+    .catch(error => {
+        console.error('保存监控配置失败:', error);
+        Swal.fire({
+            icon: 'error',
+            title: '保存失败',
+            text: '保存监控配置失败: ' + error.message,
+            confirmButtonText: '确定'
+        });
+    });
+}
+
+function toggleMonitoring() {
+    const isCurrentlyEnabled = document.getElementById('monitoringStatus').textContent === '已启用';
+    const newStatus = !isCurrentlyEnabled ? '启用' : '禁用';
+    
+    Swal.fire({
+        title: `确认${newStatus}监控?`,
+        html: `
+            <div style="text-align: left; margin-top: 10px;">
+                <p>您确定要<strong>${newStatus}</strong>容器监控系统吗?</p>
+                ${isCurrentlyEnabled ? 
+                  '<p><i class="fas fa-exclamation-triangle" style="color: #f39c12;"></i> 禁用后,系统将停止监控容器状态并停止发送通知。</p>' : 
+                  '<p><i class="fas fa-info-circle" style="color: #3498db;"></i> 启用后,系统将开始定期检查容器状态并在发现异常时发送通知。</p>'}
+            </div>
+        `,
+        icon: 'question',
+        showCancelButton: true,
+        confirmButtonColor: isCurrentlyEnabled ? '#d33' : '#3085d6',
+        cancelButtonColor: '#6c757d',
+        confirmButtonText: `确认${newStatus}`,
+        cancelButtonText: '取消'
+    }).then((result) => {
+        if (result.isConfirmed) {
+            // 显示处理中状态
+            Swal.fire({
+                title: '处理中...',
+                html: `<i class="fas fa-spinner fa-spin"></i> 正在${newStatus}监控`,
+                showConfirmButton: false,
+                allowOutsideClick: false,
+                willOpen: () => {
+                    Swal.showLoading();
+                }
+            });
+            
+            fetch('/api/toggle-monitoring', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    isEnabled: !isCurrentlyEnabled
+                })
+            })
+            .then(response => {
+                if (!response.ok) throw new Error('切换监控状态失败');
+                return response.json();
+            })
+            .then(() => {
+                loadMonitoringConfig();
+                Swal.fire({
+                    icon: 'success',
+                    title: `${newStatus}成功`,
+                    text: `监控已成功${newStatus}`,
+                    timer: 2000,
+                    showConfirmButton: false
+                });
+            })
+            .catch(error => {
+                console.error('切换监控状态失败:', error);
+                Swal.fire({
+                    icon: 'error',
+                    title: `${newStatus}失败`,
+                    text: '切换监控状态失败: ' + error.message,
+                    confirmButtonText: '确定'
+                });
+            });
+        }
+    });
+}
+
+function refreshStoppedContainers() {
+    fetch('/api/stopped-containers')
+        .then(response => {
+            if (!response.ok) throw new Error('获取已停止容器列表失败');
+            return response.json();
+        })
+        .then(containers => {
+            const tbody = document.getElementById('stoppedContainersBody');
+            tbody.innerHTML = '';
+            
+            if (containers.length === 0) {
+                tbody.innerHTML = '<tr><td colspan="3" style="text-align: center;">没有已停止的容器</td></tr>';
+                return;
+            }
+            
+            containers.forEach(container => {
+                const row = `
+                    <tr>
+                        <td>${container.id}</td>
+                        <td>${container.name}</td>
+                        <td>${container.status}</td>
+                    </tr>
+                `;
+                tbody.innerHTML += row;
+            });
+        })
+        .catch(error => {
+            console.error('获取已停止容器列表失败:', error);
+            document.getElementById('stoppedContainersBody').innerHTML = 
+                '<tr><td colspan="3" style="text-align: center; color: red;">获取已停止容器列表失败</td></tr>';
+        });
+}
+
+// 保存配置函数
+function saveConfig(configData) {
+    core.showLoading();
+    
+    fetch('/api/config', {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json'
+        },
+        body: JSON.stringify(configData)
+    })
+    .then(response => {
+        if (!response.ok) {
+            return response.text().then(text => {
+                throw new Error(`保存配置失败: ${text || response.statusText || response.status}`);
+            });
+        }
+        return response.json();
+    })
+    .then(() => {
+        core.showAlert('配置已保存', 'success');
+        // 如果更新了菜单,重新加载菜单项
+        if (configData.menuItems) {
+            menuManager.loadMenuItems();
+        }
+        // 重新加载系统配置
+        core.loadSystemConfig();
+    })
+    .catch(error => {
+        console.error('保存配置失败:', error);
+        core.showAlert('保存配置失败: ' + error.message, 'error');
+    })
+    .finally(() => {
+        core.hideLoading();
+    });
+}
+
+// 加载基本配置
+function loadBasicConfig() {
+    fetch('/api/config')
+        .then(response => {
+            if (!response.ok) throw new Error('加载配置失败');
+            return response.json();
+        })
+        .then(config => {
+            // 填充Logo URL
+            if (document.getElementById('logoUrl')) {
+                document.getElementById('logoUrl').value = config.logo || '';
+            }
+            
+            // 填充代理域名
+            if (document.getElementById('proxyDomain')) {
+                document.getElementById('proxyDomain').value = config.proxyDomain || '';
+            }
+            
+            console.log('基本配置已加载');
+        })
+        .catch(error => {
+            console.error('加载基本配置失败:', error);
+        });
+}
+
+// 暴露给全局作用域的函数
+window.app = {
+    loadMonitoringConfig,
+    loadBasicConfig,
+    toggleNotificationFields,
+    saveMonitoringConfig,
+    testNotification,
+    toggleMonitoring,
+    refreshStoppedContainers,
+    saveConfig
+};

+ 124 - 0
hubcmdui/web/js/auth.js

@@ -0,0 +1,124 @@
+// 用户认证相关功能
+
+// 登录函数
+async function login() {
+    const username = document.getElementById('username').value;
+    const password = document.getElementById('password').value;
+    const captcha = document.getElementById('captcha').value;
+            
+    try {
+        core.showLoading();
+        const response = await fetch('/api/login', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ username, password, captcha })
+        });
+        
+        if (response.ok) {
+            const data = await response.json();
+            
+            window.isLoggedIn = true;
+            localStorage.setItem('isLoggedIn', 'true');
+            persistSession();
+            document.getElementById('currentUsername').textContent = username;
+            document.getElementById('welcomeUsername').textContent = username;
+            document.getElementById('loginModal').style.display = 'none';
+            document.getElementById('adminContainer').style.display = 'flex';
+            
+            // 确保加载完成后初始化事件监听器
+            await core.loadSystemConfig();
+            core.initEventListeners();
+            core.showSection('dashboard');
+            userCenter.getUserInfo();
+            systemStatus.refreshSystemStatus();
+        } else {
+            const errorData = await response.json();
+            core.showAlert(errorData.error || '登录失败', 'error');
+            refreshCaptcha();
+        }
+    } catch (error) {
+        core.showAlert('登录失败: ' + error.message, 'error');
+        refreshCaptcha();
+    } finally {
+        core.hideLoading();
+    }
+}
+
+// 注销函数
+async function logout() {
+    console.log("注销操作被触发");
+    try {
+        core.showLoading();
+        const response = await fetch('/api/logout', { method: 'POST' });
+        if (response.ok) {
+            // 清除所有登录状态
+            localStorage.removeItem('isLoggedIn');
+            sessionStorage.removeItem('sessionActive');
+            window.isLoggedIn = false;
+            // 清除cookie
+            document.cookie = 'connect.sid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
+            window.location.reload();
+        } else {
+            throw new Error('退出登录失败');
+        }
+    } catch (error) {
+        console.error('退出登录失败:', error);
+        core.showAlert('退出登录失败: ' + error.message, 'error');
+        // 即使API失败也清除本地状态
+        localStorage.removeItem('isLoggedIn');
+        sessionStorage.removeItem('sessionActive');
+        window.isLoggedIn = false;
+        window.location.reload();
+    } finally {
+        core.hideLoading();
+    }
+}
+
+// 验证码刷新函数
+async function refreshCaptcha() {
+    try {
+        const response = await fetch('/api/captcha');
+        if (!response.ok) {
+            throw new Error(`验证码获取失败: ${response.status}`);
+        }
+        const data = await response.json();
+        document.getElementById('captchaText').textContent = data.captcha;
+    } catch (error) {
+        console.error('刷新验证码失败:', error);
+        document.getElementById('captchaText').textContent = '验证码加载失败,点击重试';
+    }
+}
+
+// 持久化会话
+function persistSession() {
+    if (document.cookie.includes('connect.sid')) {
+        sessionStorage.setItem('sessionActive', 'true');
+    }
+}
+
+// 显示登录模态框
+function showLoginModal() {
+    // 确保先隐藏加载指示器
+    if (core && typeof core.hideLoadingIndicator === 'function') {
+        core.hideLoadingIndicator();
+    }
+    
+    document.getElementById('loginModal').style.display = 'flex';
+    refreshCaptcha();
+}
+
+// 导出模块
+const auth = {
+    init: function() {
+        console.log('初始化认证模块...');
+        // 在这里可以添加认证模块初始化的相关代码
+        return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致
+    },
+    login,
+    logout,
+    refreshCaptcha,
+    showLoginModal
+};
+
+// 全局公开认证模块
+window.auth = auth;

+ 499 - 0
hubcmdui/web/js/core.js

@@ -0,0 +1,499 @@
+/**
+ * 核心功能模块
+ * 提供全局共享的工具函数和状态管理
+ */
+
+// 全局变量和状态
+let isLoggedIn = false;
+let userPermissions = [];
+let systemConfig = {};
+
+/**
+ * 初始化应用
+ * 检查登录状态,加载基础配置
+ */
+async function initApp() {
+    console.log('初始化应用...');
+    console.log('-------------调试信息开始-------------');
+    console.log('当前URL:', window.location.href);
+    console.log('浏览器信息:', navigator.userAgent);
+    console.log('DOM已加载状态:', document.readyState);
+    
+    // 检查当前页面是否为登录页
+    const isLoginPage = window.location.pathname.includes('admin');
+    console.log('是否为管理页面:', isLoginPage);
+    
+    try {
+        // 检查会话状态
+        const sessionResult = await checkSession();
+        const isAuthenticated = sessionResult.authenticated;
+        console.log('会话检查结果:', isAuthenticated);
+
+        // 检查localStorage中的登录状态 (主要用于刷新页面时保持UI)
+        const localLoginState = localStorage.getItem('isLoggedIn') === 'true';
+        
+        // 核心登录状态判断
+        if (isAuthenticated) {
+            // 已登录
+            isLoggedIn = true;
+            localStorage.setItem('isLoggedIn', 'true'); // 保持本地状态
+            
+            if (isLoginPage) {
+                // 在登录页,但会话有效,显示管理界面
+                console.log('已登录,显示管理界面...');
+                await loadSystemConfig();
+                showAdminInterface();
+            } else {
+                // 在非登录页,正常显示
+                console.log('已登录,继续应用初始化...');
+                await loadSystemConfig();
+                showAdminInterface(); // 确保管理界面可见
+            }
+        } else {
+            // 未登录
+            isLoggedIn = false;
+            localStorage.removeItem('isLoggedIn'); // 清除本地登录状态
+            
+            if (!isLoginPage) {
+                // 在非登录页,重定向到登录页
+                console.log('未登录,重定向到登录页...');
+                window.location.href = '/admin';
+                return false;
+            } else {
+                // 在登录页,显示登录框
+                console.log('未登录,显示登录模态框...');
+                hideLoadingIndicator();
+                showLoginModal();
+            }
+        }
+        
+        console.log('应用初始化完成');
+        console.log('-------------调试信息结束-------------');
+        return isAuthenticated;
+    } catch (error) {
+        console.error('初始化应用失败:', error);
+        console.log('-------------调试错误信息-------------');
+        console.log('错误堆栈:', error.stack);
+        console.log('错误类型:', error.name);
+        console.log('错误消息:', error.message);
+        console.log('---------------------------------------');
+        showAlert('加载应用失败:' + error.message, 'error');
+        hideLoadingIndicator();
+        showLoginModal();
+        return false;
+    }
+}
+
+/**
+ * 检查会话状态
+ */
+async function checkSession() {
+    try {
+        const response = await fetch('/api/check-session', {
+            headers: {
+                'Cache-Control': 'no-cache',
+                'X-Requested-With': 'XMLHttpRequest',
+                'Pragma': 'no-cache'
+            },
+            credentials: 'same-origin'
+        });
+        
+        // 只关心请求是否成功以及认证状态
+        if (response.ok) {
+            const data = await response.json();
+            return {
+                authenticated: data.authenticated // 直接使用API返回的状态
+            };
+        }
+        
+        // 非OK响应(包括401)都视为未认证
+        return {
+            authenticated: false
+        };
+    } catch (error) {
+        console.error('检查会话状态出错:', error);
+        return {
+            authenticated: false,
+            error: error.message
+        };
+    }
+}
+
+/**
+ * 加载系统配置
+ */
+function loadSystemConfig() {
+    fetch('/api/config')
+        .then(response => {
+            if (!response.ok) {
+                return response.text().then(text => {
+                    throw new Error(`加载配置失败: ${text || response.statusText || response.status}`);
+                });
+            }
+            return response.json();
+        })
+        .then(config => {
+            console.log('加载配置成功:', config);
+            // 应用配置 
+            applySystemConfig(config);
+        })
+        .catch(error => {
+            console.error('加载配置失败:', error);
+            showAlert('加载配置失败: ' + error.message, 'warning');
+        });
+}
+
+// 应用系统配置
+function applySystemConfig(config) {
+    // 如果有proxyDomain配置,则更新输入框
+    if (config.proxyDomain && document.getElementById('proxyDomain')) {
+        document.getElementById('proxyDomain').value = config.proxyDomain;
+    }
+    
+    // 应用其他配置...
+}
+
+/**
+ * 显示管理界面
+ */
+function showAdminInterface() {
+    console.log('开始显示管理界面...');
+    hideLoadingIndicator();
+    
+    const adminContainer = document.getElementById('adminContainer');
+    if (adminContainer) {
+        console.log('找到管理界面容器,设置为显示');
+        adminContainer.style.display = 'flex';
+    } else {
+        console.error('未找到管理界面容器元素 #adminContainer');
+    }
+    
+    console.log('管理界面已显示,正在初始化事件监听器');
+    
+    // 初始化菜单事件监听
+    initEventListeners();
+}
+
+/**
+ * 隐藏加载提示器
+ */
+function hideLoadingIndicator() {
+    console.log('正在隐藏加载提示器...');
+    const loadingIndicator = document.getElementById('loadingIndicator');
+    if (loadingIndicator) {
+        loadingIndicator.style.display = 'none';
+        console.log('加载提示器已隐藏');
+    } else {
+        console.warn('未找到加载提示器元素 #loadingIndicator');
+    }
+}
+
+/**
+ * 显示登录模态框
+ */
+function showLoginModal() {
+    const loginModal = document.getElementById('loginModal');
+    if (loginModal) {
+        loginModal.style.display = 'flex';
+        // 刷新验证码
+        if (window.auth && typeof window.auth.refreshCaptcha === 'function') {
+            window.auth.refreshCaptcha();
+        }
+    }
+}
+
+/**
+ * 显示加载动画
+ */
+function showLoading() {
+    const loadingSpinner = document.getElementById('loadingSpinner');
+    if (loadingSpinner) {
+        loadingSpinner.style.display = 'block';
+    }
+}
+
+/**
+ * 隐藏加载动画
+ */
+function hideLoading() {
+    const loadingSpinner = document.getElementById('loadingSpinner');
+    if (loadingSpinner) {
+        loadingSpinner.style.display = 'none';
+    }
+}
+
+/**
+ * 显示警告消息
+ * @param {string} message - 消息内容
+ * @param {string} type - 消息类型 (info, success, error)
+ * @param {string} title - 标题(可选)
+ */
+function showAlert(message, type = 'info', title = '') {
+    // 使用SweetAlert2替代自定义警告框,确保弹窗总是显示
+    Swal.fire({
+        title: title || (type === 'success' ? '成功' : (type === 'error' ? '错误' : '提示')),
+        text: message,
+        icon: type,
+        timer: type === 'success' ? 2000 : undefined,
+        timerProgressBar: type === 'success',
+        confirmButtonColor: '#3d7cf4',
+        confirmButtonText: '确定'
+    });
+}
+
+/**
+ * 显示确认对话框
+ * @param {string} message - 消息内容
+ * @param {Function} onConfirm - 确认回调
+ * @param {Function} onCancel - 取消回调(可选)
+ * @param {string} title - 标题(可选)
+ */
+function showConfirm(message, onConfirm, onCancel, title = '确认') {
+    Swal.fire({
+        title: title,
+        text: message,
+        icon: 'question',
+        showCancelButton: true,
+        confirmButtonColor: '#3d7cf4',
+        cancelButtonColor: '#6c757d',
+        confirmButtonText: '确认',
+        cancelButtonText: '取消'
+    }).then((result) => {
+        if (result.isConfirmed && typeof onConfirm === 'function') {
+            onConfirm();
+        } else if (typeof onCancel === 'function') {
+            onCancel();
+        }
+    });
+}
+
+/**
+ * 格式化日期时间
+ */
+function formatDateTime(dateString) {
+    if (!dateString) return '';
+    
+    const date = new Date(dateString);
+    return date.toLocaleString('zh-CN', {
+        year: 'numeric',
+        month: '2-digit',
+        day: '2-digit',
+        hour: '2-digit',
+        minute: '2-digit',
+        second: '2-digit'
+    });
+}
+
+/**
+ * 防抖函数:限制函数在一定时间内只能执行一次
+ */
+function debounce(func, wait = 300) {
+    let timeout;
+    return function(...args) {
+        const later = () => {
+            clearTimeout(timeout);
+            func.apply(this, args);
+        };
+        clearTimeout(timeout);
+        timeout = setTimeout(later, wait);
+    };
+}
+
+/**
+ * 节流函数:保证一定时间内多次调用只执行一次
+ */
+function throttle(func, wait = 300) {
+    let timeout = null;
+    let previous = 0;
+    
+    return function(...args) {
+        const now = Date.now();
+        const remaining = wait - (now - previous);
+        
+        if (remaining <= 0) {
+            if (timeout) {
+                clearTimeout(timeout);
+                timeout = null;
+            }
+            previous = now;
+            func.apply(this, args);
+        } else if (!timeout) {
+            timeout = setTimeout(() => {
+                previous = Date.now();
+                timeout = null;
+                func.apply(this, args);
+            }, remaining);
+        }
+    };
+}
+
+/**
+ * 初始化事件监听
+ */
+function initEventListeners() {
+    console.log('开始初始化事件监听器...');
+    
+    // 侧边栏菜单切换事件
+    const menuItems = document.querySelectorAll('.sidebar-nav li');
+    console.log('找到侧边栏菜单项数量:', menuItems.length);
+    
+    if (menuItems.length > 0) {
+        menuItems.forEach((item, index) => {
+            const sectionId = item.getAttribute('data-section');
+            console.log(`绑定事件到菜单项 #${index+1}: ${sectionId}`);
+            item.addEventListener('click', function() {
+                const sectionId = this.getAttribute('data-section');
+                showSection(sectionId);
+            });
+        });
+        console.log('侧边栏菜单事件监听器已绑定');
+    } else {
+        console.error('未找到侧边栏菜单项 .sidebar-nav li');
+    }
+    
+    // 用户中心按钮
+    const userCenterBtn = document.getElementById('userCenterBtn');
+    if (userCenterBtn) {
+        console.log('找到用户中心按钮,绑定事件');
+        userCenterBtn.addEventListener('click', () => showSection('user-center'));
+    } else {
+        console.warn('未找到用户中心按钮 #userCenterBtn');
+    }
+    
+    // 登出按钮
+    const logoutBtn = document.getElementById('logoutBtn');
+    if (logoutBtn) {
+        console.log('找到登出按钮,绑定事件');
+        logoutBtn.addEventListener('click', () => auth.logout());
+    } else {
+        console.warn('未找到登出按钮 #logoutBtn');
+    }
+    
+    // 用户中心内登出按钮
+    const ucLogoutBtn = document.getElementById('ucLogoutBtn');
+    if (ucLogoutBtn) {
+        console.log('找到用户中心内登出按钮,绑定事件');
+        ucLogoutBtn.addEventListener('click', () => auth.logout());
+    } else {
+        console.warn('未找到用户中心内登出按钮 #ucLogoutBtn');
+    }
+    
+    console.log('事件监听器初始化完成');
+}
+
+/**
+ * 显示指定的内容区域
+ * @param {string} sectionId 要显示的内容区域ID
+ */
+function showSection(sectionId) {
+    console.log(`尝试显示内容区域: ${sectionId}`);
+    
+    // 获取所有内容区域和菜单项
+    const contentSections = document.querySelectorAll('.content-section');
+    const menuItems = document.querySelectorAll('.sidebar-nav li');
+    
+    console.log(`找到 ${contentSections.length} 个内容区域和 ${menuItems.length} 个菜单项`);
+    
+    let sectionFound = false;
+    let menuItemFound = false;
+    
+    // 隐藏所有内容区域,取消激活所有菜单项
+    contentSections.forEach(section => {
+        section.classList.remove('active');
+    });
+    
+    menuItems.forEach(item => {
+        item.classList.remove('active');
+    });
+    
+    // 激活指定的内容区域
+    const targetSection = document.getElementById(sectionId);
+    if (targetSection) {
+        targetSection.classList.add('active');
+        sectionFound = true;
+        console.log(`成功激活内容区域: ${sectionId}`);
+        
+        // 特殊处理:切换到用户中心时,确保用户信息已加载
+        if (sectionId === 'user-center' && window.userCenter) {
+            console.log('切换到用户中心,调用 getUserInfo()');
+            window.userCenter.getUserInfo();
+        }
+    } else {
+        console.error(`未找到指定的内容区域: ${sectionId}`);
+    }
+    
+    // 激活相应的菜单项
+    const targetMenuItem = document.querySelector(`.sidebar-nav li[data-section="${sectionId}"]`);
+    if (targetMenuItem) {
+        targetMenuItem.classList.add('active');
+        menuItemFound = true;
+        console.log(`成功激活菜单项: ${sectionId}`);
+    } else {
+        console.error(`未找到对应的菜单项: ${sectionId}`);
+    }
+    
+    // 如果没有找到指定的内容区域和菜单项,显示仪表盘
+    if (!sectionFound && !menuItemFound) {
+        console.warn(`未找到指定的内容区域和菜单项,将显示默认仪表盘`);
+        const dashboard = document.getElementById('dashboard');
+        if (dashboard) {
+            dashboard.classList.add('active');
+            const dashboardMenuItem = document.querySelector('.sidebar-nav li[data-section="dashboard"]');
+            if (dashboardMenuItem) {
+                dashboardMenuItem.classList.add('active');
+            }
+        }
+    }
+    
+    // 切换内容区域后可能需要执行的额外操作
+    if (sectionId === 'dashboard') {
+        console.log('已激活仪表盘,无需再次刷新系统状态');
+        // 不再自动刷新系统状态,仅在首次加载或用户手动点击刷新按钮时刷新
+    }
+    
+    console.log(`内容区域切换完成: ${sectionId}`);
+}
+
+// 页面加载时初始化
+document.addEventListener('DOMContentLoaded', function() {
+    console.log('DOM已加载,正在初始化应用...');
+    initApp();
+    
+    // 检查URL参数,处理消息提示等
+    const urlParams = new URLSearchParams(window.location.search);
+    
+    // 如果有message参数,显示相应的提示
+    if (urlParams.has('message')) {
+        const message = urlParams.get('message');
+        let type = 'info';
+        
+        if (urlParams.has('type')) {
+            type = urlParams.get('type');
+        }
+        
+        showAlert(message, type);
+    }
+});
+
+// 导出核心对象
+const core = {
+    isLoggedIn,
+    initApp,
+    checkSession,
+    loadSystemConfig,
+    applySystemConfig,
+    showLoading,
+    hideLoading,
+    hideLoadingIndicator,
+    showLoginModal,
+    showAlert,
+    showConfirm,
+    formatDateTime,
+    debounce,
+    throttle,
+    initEventListeners,
+    showSection
+};
+
+// 全局公开核心模块
+window.core = core;

+ 681 - 0
hubcmdui/web/js/dockerManager.js

@@ -0,0 +1,681 @@
+/**
+ * Docker管理模块 - 专注于 Docker 容器表格的渲染和交互
+ */
+
+const dockerManager = {
+    // 初始化函数 - 只做基本的 UI 设置或事件监听(如果需要)
+    init: function() {
+        // 减少日志输出
+        // console.log('[dockerManager] Initializing Docker manager UI components...');
+        
+        // 可以在这里添加下拉菜单的全局事件监听器等
+        this.setupActionDropdownListener(); 
+        
+        // 立即显示加载状态和表头
+        this.showLoadingState();
+        
+        // 添加对Bootstrap下拉菜单的初始化
+        document.addEventListener('DOMContentLoaded', () => {
+            this.initDropdowns();
+        });
+        
+        // 当文档已经加载完成时立即初始化
+        if (document.readyState === 'complete' || document.readyState === 'interactive') {
+            this.initDropdowns();
+        }
+        
+        return Promise.resolve();
+    },
+
+    // 初始化Bootstrap下拉菜单组件
+    initDropdowns: function() {
+        // 减少日志输出
+        // console.log('[dockerManager] Initializing Bootstrap dropdowns...');
+        
+        // 直接初始化,不使用setTimeout避免延迟导致的问题
+        try {
+            // 动态初始化所有下拉菜单
+            const dropdownElements = document.querySelectorAll('[data-bs-toggle="dropdown"]');
+            if (dropdownElements.length === 0) {
+                return; // 如果没有找到下拉元素,直接返回
+            }
+            
+            if (window.bootstrap && window.bootstrap.Dropdown) {
+                dropdownElements.forEach(el => {
+                    try {
+                        new window.bootstrap.Dropdown(el);
+                    } catch (e) {
+                        // 静默处理错误,不要输出到控制台
+                    }
+                });
+            } else {
+                console.warn('Bootstrap Dropdown 组件未找到,将尝试使用jQuery初始化');
+                // 尝试使用jQuery初始化(如果存在)
+                if (window.jQuery) {
+                    window.jQuery('[data-bs-toggle="dropdown"]').dropdown();
+                }
+            }
+        } catch (error) {
+            // 静默处理错误
+        }
+    },
+
+    // 显示表格加载状态 - 保持,用于初始渲染和刷新
+    showLoadingState() {
+        const table = document.getElementById('dockerStatusTable');
+        const tbody = document.getElementById('dockerStatusTableBody');
+        
+        // 首先创建表格标题区域(如果不存在)
+        let tableContainer = document.getElementById('dockerTableContainer');
+        if (tableContainer) {
+            // 添加表格标题区域 - 只有不存在时才添加
+            if (!tableContainer.querySelector('.docker-table-header')) {
+                const tableHeader = document.createElement('div');
+                tableHeader.className = 'docker-table-header';
+                tableHeader.innerHTML = `
+                    <h2 class="docker-table-title">Docker 容器管理</h2>
+                    <div class="docker-table-actions">
+                        <button id="refreshDockerBtn" class="btn btn-sm btn-primary">
+                            <i class="fas fa-sync-alt me-1"></i> 刷新列表
+                        </button>
+                    </div>
+                `;
+                
+                // 插入到表格前面
+                if (table) {
+                    tableContainer.insertBefore(tableHeader, table);
+                    
+                    // 添加刷新按钮事件
+                    const refreshBtn = document.getElementById('refreshDockerBtn');
+                    if (refreshBtn) {
+                        refreshBtn.addEventListener('click', () => {
+                            if (window.systemStatus && typeof window.systemStatus.refreshSystemStatus === 'function') {
+                                window.systemStatus.refreshSystemStatus();
+                            }
+                        });
+                    }
+                }
+            }
+        }
+        
+        if (table && tbody) {
+            // 添加Excel风格表格类
+            table.classList.add('excel-table');
+            
+            // 确保表头存在并正确渲染
+            const thead = table.querySelector('thead');
+            if (thead) {
+                thead.innerHTML = ` 
+                    <tr>
+                        <th style="width: 120px;">容器ID</th>
+                        <th style="width: 25%;">容器名称</th>
+                        <th style="width: 35%;">镜像名称</th>
+                        <th style="width: 100px;">运行状态</th>
+                        <th style="width: 150px;">操作</th>
+                    </tr>
+                `;
+            }
+            
+            // 显示加载状态
+            tbody.innerHTML = `
+                <tr class="loading-container">
+                    <td colspan="5">
+                        <div class="loading-animation">
+                            <div class="spinner"></div>
+                            <p>正在加载容器列表...</p>
+                        </div>
+                    </td>
+                </tr>
+            `;
+        }
+    },
+
+    // 渲染容器表格 - 核心渲染函数,由 systemStatus 调用
+    renderContainersTable(containers, dockerStatus) {
+        // 减少详细日志输出
+        // console.log(`[dockerManager] Rendering containers table. Containers count: ${containers ? containers.length : 0}`);
+        
+        const tbody = document.getElementById('dockerStatusTableBody');
+        if (!tbody) {
+            return;
+        }
+        
+        // 确保表头存在 (showLoadingState 应该已经创建)
+        const table = document.getElementById('dockerStatusTable');
+        if (table) {
+            const thead = table.querySelector('thead');
+            if (!thead || !thead.querySelector('tr')) {
+                // 重新创建表头
+                const newThead = thead || document.createElement('thead');
+                newThead.innerHTML = ` 
+                    <tr>
+                        <th style="width: 120px;">容器ID</th>
+                        <th style="width: 25%;">容器名称</th>
+                        <th style="width: 35%;">镜像名称</th>
+                        <th style="width: 100px;">运行状态</th>
+                        <th style="width: 150px;">操作</th>
+                    </tr>
+                `;
+                
+                if (!thead) {
+                    table.insertBefore(newThead, tbody);
+                }
+            }
+        }
+
+        // 1. 检查 Docker 服务状态
+        if (dockerStatus !== 'running') {
+            tbody.innerHTML = `
+                <tr>
+                    <td colspan="5" class="text-center text-muted py-4">
+                        <i class="fab fa-docker fa-lg me-2"></i> Docker 服务未运行
+                    </td>
+                </tr>
+            `;
+            return;
+        }
+
+        // 2. 检查容器数组是否有效且有内容
+        if (!Array.isArray(containers) || containers.length === 0) {
+            tbody.innerHTML = `
+                <tr>
+                    <td colspan="5" class="text-center text-muted py-4">
+                         <i class="fas fa-info-circle me-2"></i> 暂无运行中的Docker容器
+                    </td>
+                </tr>
+            `;
+            return;
+        }
+
+        // 3. 渲染容器列表
+        let html = '';
+        containers.forEach(container => {
+            const status = container.State || container.status || '未知';
+            const statusClass = this.getContainerStatusClass(status);
+            const containerId = container.Id || container.id || '未知';
+            const containerName = container.Names?.[0]?.substring(1) || container.name || '未知';
+            const containerImage = container.Image || container.image || '未知';
+            
+            // 添加lowerStatus变量定义,修复错误
+            const lowerStatus = status.toLowerCase();
+
+            // 替换下拉菜单实现为直接的操作按钮
+            let actionButtons = '';
+            
+            // 基本操作:查看日志和详情
+            actionButtons += `
+                <button class="btn btn-sm btn-outline-info mb-1 mr-1 action-logs" data-id="${containerId}" data-name="${containerName}">
+                    <i class="fas fa-file-alt"></i> 日志
+                </button>
+                <button class="btn btn-sm btn-outline-secondary mb-1 mr-1 action-details" data-id="${containerId}">
+                    <i class="fas fa-info-circle"></i> 详情
+                </button>
+            `;
+            
+            // 根据状态显示不同操作
+            if (lowerStatus.includes('running')) {
+                actionButtons += `
+                    <button class="btn btn-sm btn-outline-warning mb-1 mr-1 action-stop" data-id="${containerId}">
+                        <i class="fas fa-stop"></i> 停止
+                    </button>
+                    <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-restart" data-id="${containerId}">
+                        <i class="fas fa-sync-alt"></i> 重启
+                    </button>
+                `;
+            } else if (lowerStatus.includes('exited') || lowerStatus.includes('stopped') || lowerStatus.includes('created')) {
+                actionButtons += `
+                    <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-start" data-id="${containerId}">
+                        <i class="fas fa-play"></i> 启动
+                    </button>
+                    <button class="btn btn-sm btn-outline-danger mb-1 mr-1 action-remove" data-id="${containerId}">
+                        <i class="fas fa-trash-alt"></i> 删除
+                    </button>
+                `;
+            } else if (lowerStatus.includes('paused')) {
+                actionButtons += `
+                    <button class="btn btn-sm btn-outline-success mb-1 mr-1 action-unpause" data-id="${containerId}">
+                        <i class="fas fa-play"></i> 恢复
+                    </button>
+                `;
+            }
+            
+            // 更新容器按钮(总是显示)
+            actionButtons += `
+                <button class="btn btn-sm btn-outline-primary mb-1 mr-1 action-update" data-id="${containerId}" data-image="${containerImage || ''}">
+                    <i class="fas fa-cloud-download-alt"></i> 更新
+                </button>
+            `;
+
+            html += `
+                <tr>
+                    <td data-label="ID" title="${containerId}">${containerId.substring(0, 12)}</td>
+                    <td data-label="名称" title="${containerName}">${containerName}</td>
+                    <td data-label="镜像" title="${containerImage}">${containerImage}</td>
+                    <td data-label="状态"><span class="badge ${statusClass}">${status}</span></td>
+                    <td data-label="操作" class="action-cell">
+                        <div class="action-buttons">
+                            ${actionButtons}
+                        </div>
+                    </td>
+                </tr>
+            `;
+        });
+        
+        tbody.innerHTML = html;
+        
+        // 为所有操作按钮绑定事件
+        this.setupButtonListeners();
+    },
+    
+    // 为所有操作按钮绑定事件
+    setupButtonListeners() {
+        // 查找所有操作按钮并绑定点击事件
+        document.querySelectorAll('.action-cell button').forEach(button => {
+            const action = Array.from(button.classList).find(cls => cls.startsWith('action-'));
+            if (!action) return;
+            
+            const containerId = button.dataset.id;
+            if (!containerId) return;
+            
+            button.addEventListener('click', (event) => {
+                event.preventDefault();
+                const containerName = button.dataset.name;
+                const containerImage = button.dataset.image;
+                
+                switch (action) {
+                    case 'action-logs':
+                        this.showContainerLogs(containerId, containerName);
+                        break;
+                    case 'action-details':
+                        this.showContainerDetails(containerId);
+                        break;
+                    case 'action-stop':
+                        this.stopContainer(containerId);
+                        break;
+                    case 'action-start':
+                        this.startContainer(containerId);
+                        break;
+                    case 'action-restart':
+                        this.restartContainer(containerId);
+                        break;
+                    case 'action-remove':
+                        this.removeContainer(containerId);
+                        break;
+                    case 'action-unpause':
+                        // this.unpauseContainer(containerId); // 假设有这个函数
+                        console.warn('Unpause action not implemented yet.');
+                        break;
+                    case 'action-update':
+                        this.updateContainer(containerId, containerImage);
+                        break;
+                    default:
+                        console.warn('Unknown action:', action);
+                }
+            });
+        });
+    },
+    
+    // 获取容器状态对应的 CSS 类 - 保持
+    getContainerStatusClass(state) {
+        if (!state) return 'status-unknown';
+        state = state.toLowerCase();
+        if (state.includes('running')) return 'status-running';
+        if (state.includes('created')) return 'status-created';
+        if (state.includes('exited') || state.includes('stopped')) return 'status-stopped';
+        if (state.includes('paused')) return 'status-paused';
+        return 'status-unknown';
+    },
+    
+    // 设置下拉菜单动作的事件监听 (委托方法 - 现在直接使用按钮,不再需要)
+    setupActionDropdownListener() {
+        // 这个方法留作兼容性,但实际上我们现在直接使用按钮而非下拉菜单
+    },
+
+    // 查看日志 (示例:用 SweetAlert 显示)
+    async showContainerLogs(containerId, containerName) {
+        core.showLoading('正在加载日志...');
+        try {
+            // 注意: 后端 /api/docker/containers/:id/logs 需要存在并返回日志文本
+            const response = await fetch(`/api/docker/containers/${containerId}/logs`); 
+            if (!response.ok) {
+                const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
+                throw new Error(errorData.details || `获取日志失败 (${response.status})`);
+            }
+            const logs = await response.text();
+            core.hideLoading();
+            
+            Swal.fire({
+                title: `容器日志: ${containerName || containerId.substring(0, 6)}`,
+                html: `<pre class="container-logs">${logs.replace(/</g, "&lt;").replace(/>/g, "&gt;")}</pre>`,
+                width: '80%',
+                customClass: {
+                    htmlContainer: 'swal2-logs-container',
+                    popup: 'swal2-logs-popup'
+                },
+                confirmButtonText: '关闭'
+            });
+        } catch (error) {
+            core.hideLoading();
+            core.showAlert(`查看日志失败: ${error.message}`, 'error');
+            logger.error(`[dockerManager] Error fetching logs for ${containerId}:`, error);
+        }
+    },
+    
+    // 显示容器详情 (示例:用 SweetAlert 显示)
+    async showContainerDetails(containerId) {
+        core.showLoading('正在加载详情...');
+        try {
+            // 注意: 后端 /api/docker/containers/:id 需要存在并返回详细信息
+            const response = await fetch(`/api/docker/containers/${containerId}`); 
+            if (!response.ok) {
+                 const errorData = await response.json().catch(() => ({ details: '无法解析错误响应' }));
+                throw new Error(errorData.details || `获取详情失败 (${response.status})`);
+            }
+            const details = await response.json();
+            core.hideLoading();
+            
+            // 格式化显示详情
+            let detailsHtml = '<div class="container-details">';
+            for (const key in details) {
+                detailsHtml += `<p><strong>${key}:</strong> ${JSON.stringify(details[key], null, 2)}</p>`;
+            }
+            detailsHtml += '</div>';
+            
+            Swal.fire({
+                title: `容器详情: ${details.Name || containerId.substring(0, 6)}`,
+                html: detailsHtml,
+                width: '80%',
+                confirmButtonText: '关闭'
+            });
+        } catch (error) {
+            core.hideLoading();
+            core.showAlert(`查看详情失败: ${error.message}`, 'error');
+            logger.error(`[dockerManager] Error fetching details for ${containerId}:`, error);
+        }
+    },
+
+    // 启动容器
+    async startContainer(containerId) {
+        core.showLoading('正在启动容器...');
+        try {
+            const response = await fetch(`/api/docker/containers/${containerId}/start`, { method: 'POST' });
+            const data = await response.json();
+            core.hideLoading();
+            if (!response.ok) throw new Error(data.details || '启动容器失败');
+            core.showAlert('容器启动成功', 'success');
+            systemStatus.refreshSystemStatus(); // 刷新整体状态
+        } catch (error) {
+            core.hideLoading();
+            core.showAlert(`启动容器失败: ${error.message}`, 'error');
+            logger.error(`[dockerManager] Error starting container ${containerId}:`, error);
+        }
+    },
+    
+    // 停止容器
+    async stopContainer(containerId) {
+        core.showLoading('正在停止容器...');
+        try {
+            const response = await fetch(`/api/docker/containers/${containerId}/stop`, { method: 'POST' });
+            const data = await response.json();
+            core.hideLoading();
+            if (!response.ok && response.status !== 304) { // 304 Not Modified 也算成功(已停止)
+                 throw new Error(data.details || '停止容器失败');
+            }
+            core.showAlert(data.message || '容器停止成功', 'success');
+            systemStatus.refreshSystemStatus(); // 刷新整体状态
+        } catch (error) {
+            core.hideLoading();
+            core.showAlert(`停止容器失败: ${error.message}`, 'error');
+            logger.error(`[dockerManager] Error stopping container ${containerId}:`, error);
+        }
+    },
+    
+    // 重启容器
+    async restartContainer(containerId) {
+        core.showLoading('正在重启容器...');
+        try {
+            const response = await fetch(`/api/docker/containers/${containerId}/restart`, { method: 'POST' });
+            const data = await response.json();
+            core.hideLoading();
+            if (!response.ok) throw new Error(data.details || '重启容器失败');
+            core.showAlert('容器重启成功', 'success');
+            systemStatus.refreshSystemStatus(); // 刷新整体状态
+        } catch (error) {
+            core.hideLoading();
+            core.showAlert(`重启容器失败: ${error.message}`, 'error');
+             logger.error(`[dockerManager] Error restarting container ${containerId}:`, error);
+        }
+    },
+
+    // 删除容器 (带确认)
+    removeContainer(containerId) {
+        Swal.fire({
+            title: '确认删除?',
+            text: `确定要删除容器 ${containerId.substring(0, 6)} 吗?此操作不可恢复!`,
+            icon: 'warning',
+            showCancelButton: true,
+            confirmButtonColor: 'var(--danger-color)',
+            cancelButtonColor: '#6c757d',
+            confirmButtonText: '确认删除',
+            cancelButtonText: '取消'
+        }).then(async (result) => {
+            if (result.isConfirmed) {
+                core.showLoading('正在删除容器...');
+                try {
+                    const response = await fetch(`/api/docker/containers/${containerId}/remove`, { method: 'POST' }); // 使用 remove
+                    const data = await response.json();
+                    core.hideLoading();
+                    if (!response.ok) throw new Error(data.details || '删除容器失败');
+                    core.showAlert(data.message || '容器删除成功', 'success');
+                    systemStatus.refreshSystemStatus(); // 刷新整体状态
+                } catch (error) {
+                    core.hideLoading();
+                    core.showAlert(`删除容器失败: ${error.message}`, 'error');
+                    logger.error(`[dockerManager] Error removing container ${containerId}:`, error);
+                }
+            }
+        });
+    },
+
+    // --- 新增:更新容器函数 ---
+    async updateContainer(containerId, currentImage) {
+        const imageName = currentImage.split(':')[0]; // 提取基础镜像名
+        
+        const { value: newTag } = await Swal.fire({
+            title: `更新容器: ${imageName}`,
+            input: 'text',
+            inputLabel: '请输入新的镜像标签 (例如 latest, v1.2)',
+            inputValue: 'latest', // 默认值
+            showCancelButton: true,
+            confirmButtonText: '开始更新',
+            cancelButtonText: '取消',
+            confirmButtonColor: '#3085d6',
+            cancelButtonColor: '#d33',
+            inputValidator: (value) => {
+                if (!value || value.trim() === '') {
+                    return '镜像标签不能为空!';
+                }
+            },
+            // 美化弹窗样式
+            customClass: {
+                container: 'update-container',
+                popup: 'update-popup',
+                header: 'update-header',
+                title: 'update-title',
+                closeButton: 'update-close',
+                icon: 'update-icon',
+                image: 'update-image',
+                content: 'update-content',
+                input: 'update-input',
+                actions: 'update-actions',
+                confirmButton: 'update-confirm',
+                cancelButton: 'update-cancel',
+                footer: 'update-footer'
+            }
+        });
+
+        if (newTag) {
+            // 显示进度弹窗
+            Swal.fire({
+                title: '更新容器',
+                html: `
+                    <div class="update-progress">
+                        <p>正在更新容器 <strong>${containerId.substring(0, 8)}</strong></p>
+                        <p>镜像: <strong>${imageName}:${newTag.trim()}</strong></p>
+                        <div class="progress-status">准备中...</div>
+                        <div class="progress-container">
+                            <div class="progress-bar"></div>
+                        </div>
+                    </div>
+                `,
+                showConfirmButton: false,
+                allowOutsideClick: false,
+                allowEscapeKey: false,
+                didOpen: () => {
+                    const progressBar = Swal.getPopup().querySelector('.progress-bar');
+                    const progressStatus = Swal.getPopup().querySelector('.progress-status');
+                    
+                    // 设置初始进度
+                    progressBar.style.width = '0%';
+                    progressBar.style.backgroundColor = '#4CAF50';
+                    
+                    // 模拟进度动画
+                    let progress = 0;
+                    const progressInterval = setInterval(() => {
+                        // 进度最多到95%,剩下的在请求完成后处理
+                        if (progress < 95) {
+                            progress += Math.random() * 3;
+                            if (progress > 95) progress = 95;
+                            progressBar.style.width = `${progress}%`;
+                            
+                            // 更新状态文本
+                            if (progress < 30) {
+                                progressStatus.textContent = "拉取新镜像...";
+                            } else if (progress < 60) {
+                                progressStatus.textContent = "准备更新容器...";
+                            } else if (progress < 90) {
+                                progressStatus.textContent = "应用新配置...";
+                            } else {
+                                progressStatus.textContent = "即将完成...";
+                            }
+                        }
+                    }, 300);
+                    
+                    // 发送更新请求
+                    this.performContainerUpdate(containerId, newTag.trim(), progressBar, progressStatus, progressInterval);
+                }
+            });
+        }
+    },
+    
+    // 执行容器更新请求
+    async performContainerUpdate(containerId, newTag, progressBar, progressStatus, progressInterval) {
+        try {
+            const response = await fetch(`/api/docker/containers/${containerId}/update`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({ tag: newTag })
+            });
+            
+            // 清除进度定时器
+            clearInterval(progressInterval);
+            
+            if (response.ok) {
+                const data = await response.json();
+                
+                // 设置进度为100%
+                progressBar.style.width = '100%';
+                progressStatus.textContent = "更新完成!";
+                
+                // 显示成功消息
+                setTimeout(() => {
+                    Swal.fire({
+                        icon: 'success',
+                        title: '更新成功!',
+                        text: data.message || '容器已成功更新',
+                        confirmButtonText: '确定'
+                    });
+                    
+                    // 刷新容器列表
+                    systemStatus.refreshSystemStatus();
+                }, 800);
+            } else {
+                const data = await response.json().catch(() => ({ error: '解析响应失败', details: '服务器返回了无效的数据' }));
+                
+                // 设置进度条为错误状态
+                progressBar.style.width = '100%';
+                progressBar.style.backgroundColor = '#f44336';
+                progressStatus.textContent = "更新失败";
+                
+                // 显示错误消息
+                setTimeout(() => {
+                    Swal.fire({
+                        icon: 'error',
+                        title: '更新失败',
+                        text: data.details || data.error || '未知错误',
+                        confirmButtonText: '确定'
+                    });
+                }, 800);
+            }
+        } catch (error) {
+            // 清除进度定时器
+            clearInterval(progressInterval);
+            
+            // 设置进度条为错误状态
+            progressBar.style.width = '100%';
+            progressBar.style.backgroundColor = '#f44336';
+            progressStatus.textContent = "更新出错";
+            
+            // 显示错误信息
+            setTimeout(() => {
+                Swal.fire({
+                    icon: 'error',
+                    title: '更新失败',
+                    text: error.message || '网络请求失败',
+                    confirmButtonText: '确定'
+                });
+            }, 800);
+            
+            // 记录错误日志
+            logger.error(`[dockerManager] Error updating container ${containerId} to tag ${newTag}:`, error);
+        }
+    },
+
+    // --- 新增:绑定排查按钮事件 ---
+    bindTroubleshootButton() {
+        // 使用 setTimeout 确保按钮已经渲染到 DOM 中
+        setTimeout(() => {
+            const troubleshootBtn = document.getElementById('docker-troubleshoot-btn');
+            if (troubleshootBtn) {
+                // 先移除旧监听器,防止重复绑定
+                troubleshootBtn.replaceWith(troubleshootBtn.cloneNode(true));
+                const newBtn = document.getElementById('docker-troubleshoot-btn'); // 重新获取克隆后的按钮
+                if(newBtn) { 
+                    newBtn.addEventListener('click', () => {
+                        if (window.systemStatus && typeof window.systemStatus.showDockerHelp === 'function') {
+                            window.systemStatus.showDockerHelp();
+                        } else {
+                            console.error('[dockerManager] systemStatus.showDockerHelp is not available.');
+                            // 可以提供一个备用提示
+                            alert('无法显示帮助信息,请检查控制台。');
+                        }
+                    });
+                    console.log('[dockerManager] Troubleshoot button event listener bound.');
+                } else {
+                     console.warn('[dockerManager] Cloned troubleshoot button not found after replace.');
+                }
+            } else {
+                console.warn('[dockerManager] Troubleshoot button not found for binding.');
+            }
+        }, 0); // 延迟 0ms 执行,让浏览器有机会渲染
+    }
+};
+
+// 确保在 DOM 加载后初始化
+document.addEventListener('DOMContentLoaded', () => {
+    // 注意:init 现在只设置监听器,不加载数据
+    // dockerManager.init(); 
+    // 可以在 app.js 或 systemStatus.js 初始化完成后调用
+});

+ 958 - 0
hubcmdui/web/js/documentManager.js

@@ -0,0 +1,958 @@
+/**
+ * 文档管理模块
+ */
+
+// 文档列表
+let documents = [];
+// 当前正在编辑的文档
+let currentDocument = null;
+// Markdown编辑器实例
+let editorMd = null;
+
+// 创建documentManager对象
+const documentManager = {
+    // 初始化文档管理
+    init: function() {
+        console.log('初始化文档管理模块...');
+        // 渲染表头
+        this.renderDocumentTableHeader();
+        // 加载文档列表
+        return this.loadDocuments().catch(err => {
+            console.error('加载文档列表失败:', err);
+            return Promise.resolve(); // 即使失败也继续初始化过程
+        });
+    },
+
+    // 渲染文档表格头部
+    renderDocumentTableHeader: function() {
+        try {
+            const documentTable = document.getElementById('documentTable');
+            if (!documentTable) {
+                console.warn('文档管理表格元素 (id=\"documentTable\") 未找到,无法渲染表头。');
+                return;
+            }
+            
+            // 查找或创建 thead
+            let thead = documentTable.querySelector('thead');
+            if (!thead) {
+                thead = document.createElement('thead');
+                documentTable.insertBefore(thead, documentTable.firstChild); // 确保 thead 在 tbody 之前
+                console.log('创建了文档表格的 thead 元素。');
+            }
+            
+            // 设置表头内容 (包含 ID 列)
+            thead.innerHTML = `
+                <tr>
+                    <th style="width: 5%">#</th>
+                    <th style="width: 25%">标题</th>
+                    <th style="width: 20%">创建时间</th>
+                    <th style="width: 20%">更新时间</th>
+                    <th style="width: 10%">状态</th>
+                    <th style="width: 20%">操作</th>
+                </tr>
+            `;
+            console.log('文档表格表头已渲染。');
+        } catch (error) {
+            console.error('渲染文档表格表头时出错:', error);
+        }
+    },
+
+    // 加载文档列表
+    loadDocuments: async function() {
+        try {
+            // 显示加载状态
+            const documentTableBody = document.getElementById('documentTableBody');
+            if (documentTableBody) {
+                documentTableBody.innerHTML = '<tr><td colspan="6" style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> 正在加载文档列表...</td></tr>';
+            }
+            
+            // 简化会话检查逻辑,只验证会话是否有效
+            let sessionValid = true;
+            try {
+                const sessionResponse = await fetch('/api/check-session', {
+                    headers: { 
+                        'Cache-Control': 'no-cache',
+                        'X-Requested-With': 'XMLHttpRequest'
+                    },
+                    credentials: 'same-origin'
+                });
+                
+                if (sessionResponse.status === 401) {
+                    console.warn('会话已过期,无法加载文档');
+                    sessionValid = false;
+                }
+            } catch (sessionError) {
+                console.warn('检查会话状态发生网络错误:', sessionError);
+                // 发生网络错误时继续尝试加载文档
+            }
+            
+            // 尝试不同的API路径
+            const possiblePaths = [
+                '/api/documents', 
+                '/api/documentation-list', 
+                '/api/documentation'
+            ];
+            
+            let success = false;
+            let authError = false;
+            
+            for (const path of possiblePaths) {
+                try {
+                    console.log(`尝试从 ${path} 获取文档列表`);
+                    const response = await fetch(path, {
+                        credentials: 'same-origin',
+                        headers: {
+                            'Cache-Control': 'no-cache',
+                            'X-Requested-With': 'XMLHttpRequest'
+                        }
+                    });
+                    
+                    if (response.status === 401) {
+                        console.warn(`API路径 ${path} 返回未授权状态`);
+                        authError = true;
+                        continue;
+                    }
+                    
+                    if (response.ok) {
+                        const data = await response.json();
+                        documents = Array.isArray(data) ? data : [];
+                        
+                        // 确保每个文档都包含必要的时间字段
+                        documents = documents.map(doc => {
+                            if (!doc.createdAt && doc.updatedAt) {
+                                // 如果没有创建时间但有更新时间,使用更新时间
+                                doc.createdAt = doc.updatedAt;
+                            } else if (!doc.createdAt) {
+                                // 如果都没有,使用当前时间
+                                doc.createdAt = new Date().toISOString();
+                            }
+                            
+                            if (!doc.updatedAt) {
+                                // 如果没有更新时间,使用创建时间
+                                doc.updatedAt = doc.createdAt;
+                            }
+                            
+                            return doc;
+                        });
+                        
+                        // 先渲染表头,再渲染文档列表
+                        this.renderDocumentTableHeader();
+                        this.renderDocumentList();
+                        console.log(`成功从API路径 ${path} 加载文档列表`, documents);
+                        success = true;
+                        break;
+                    }
+                } catch (e) {
+                    console.warn(`从 ${path} 加载文档失败:`, e);
+                }
+            }
+            
+            // 处理认证错误 - 只有当会话检查和API请求都明确失败时才强制登出
+            if ((authError || !sessionValid) && !success && localStorage.getItem('isLoggedIn') === 'true') {
+                console.warn('会话检查和API请求均指示会话已过期');
+                core.showAlert('会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            // 如果API请求失败但不是认证错误,显示空文档列表
+            if (!success) {
+                console.log('API请求失败,显示空文档列表');
+                documents = [];
+                // 仍然需要渲染表头
+                this.renderDocumentTableHeader();
+                this.renderDocumentList();
+            }
+        } catch (error) {
+            console.error('加载文档失败:', error);
+            // 在UI上显示错误
+            const documentTableBody = document.getElementById('documentTableBody');
+            if (documentTableBody) {
+                documentTableBody.innerHTML = `<tr><td colspan="6" style="text-align: center; color: red;">加载文档失败: ${error.message} <button onclick="documentManager.loadDocuments()">重试</button></td></tr>`;
+            }
+            
+            // 设置为空文档列表
+            documents = [];
+        }
+    },
+
+    // 初始化编辑器
+    initEditor: function() {
+        try {
+            const editorContainer = document.getElementById('editor');
+            if (!editorContainer) {
+                console.error('找不到编辑器容器元素');
+                return;
+            }
+            
+            // 检查 toastui 是否已加载
+            console.log('检查编辑器依赖项:', typeof toastui);
+            
+            // 确保 toastui 对象存在
+            if (typeof toastui === 'undefined') {
+                console.error('Toast UI Editor 未加载');
+                return;
+            }
+            
+            // 创建编辑器实例
+            editorMd = new toastui.Editor({
+                el: editorContainer,
+                height: '600px',
+                initialValue: '',
+                previewStyle: 'vertical',
+                initialEditType: 'markdown',
+                toolbarItems: [
+                    ['heading', 'bold', 'italic', 'strike'],
+                    ['hr', 'quote'],
+                    ['ul', 'ol', 'task', 'indent', 'outdent'],
+                    ['table', 'image', 'link'],
+                    ['code', 'codeblock']
+                ]
+            });
+
+            console.log('编辑器初始化完成', editorMd);
+        } catch (error) {
+            console.error('初始化编辑器出错:', error);
+            core.showAlert('初始化编辑器失败: ' + error.message, 'error');
+        }
+    },
+
+    // 检查编辑器是否已初始化
+    isEditorInitialized: function() {
+        return editorMd !== null;
+    },
+
+    // 创建新文档
+    newDocument: function() {
+        // 首先确保编辑器已初始化
+        if (!editorMd) {
+            this.initEditor();
+            // 等待编辑器初始化完成后再继续
+            setTimeout(() => {
+                currentDocument = null;
+                document.getElementById('documentTitle').value = '';
+                editorMd.setMarkdown('');
+                this.showEditor();
+            }, 500);
+        } else {
+            currentDocument = null;
+            document.getElementById('documentTitle').value = '';
+            editorMd.setMarkdown('');
+            this.showEditor();
+        }
+    },
+
+    // 显示编辑器
+    showEditor: function() {
+        document.getElementById('documentTable').style.display = 'none';
+        document.getElementById('editorContainer').style.display = 'block';
+        if (editorMd) {
+            // 确保每次显示编辑器时都切换到编辑模式
+            editorMd.focus();
+        }
+    },
+
+    // 隐藏编辑器
+    hideEditor: function() {
+        document.getElementById('documentTable').style.display = 'table';
+        document.getElementById('editorContainer').style.display = 'none';
+    },
+
+    // 取消编辑
+    cancelEdit: function() {
+        this.hideEditor();
+    },
+
+    // 保存文档
+    saveDocument: async function() {
+        const title = document.getElementById('documentTitle').value.trim();
+        const content = editorMd.getMarkdown();
+        
+        if (!title) {
+            core.showAlert('请输入文档标题', 'error');
+            return;
+        }
+        
+        // 显示保存中状态
+        core.showLoading();
+        
+        try {
+            // 简化会话检查逻辑,只验证会话是否有效
+            let sessionValid = true;
+            try {
+                const sessionResponse = await fetch('/api/check-session', {
+                    headers: { 
+                        'Cache-Control': 'no-cache',
+                        'X-Requested-With': 'XMLHttpRequest'
+                    },
+                    credentials: 'same-origin'
+                });
+                
+                if (sessionResponse.status === 401) {
+                    console.warn('会话已过期,无法保存文档');
+                    sessionValid = false;
+                }
+            } catch (sessionError) {
+                console.warn('检查会话状态发生网络错误:', sessionError);
+                // 发生网络错误时继续尝试保存操作
+            }
+            
+            // 只有在会话明确无效时才退出
+            if (!sessionValid) {
+                core.showAlert('您的会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            // 确保Markdown内容以标题开始
+            let processedContent = content;
+            if (!content.startsWith('# ')) {
+                // 如果内容不是以一级标题开始,则在开头添加标题
+                processedContent = `# ${title}\n\n${content}`;
+            } else {
+                // 如果已经有一级标题,替换为当前标题
+                processedContent = content.replace(/^# .*$/m, `# ${title}`);
+            }
+            
+            const apiUrl = currentDocument && currentDocument.id 
+                ? `/api/documents/${currentDocument.id}` 
+                : '/api/documents';
+            
+            const method = currentDocument && currentDocument.id ? 'PUT' : 'POST';
+            
+            console.log(`尝试${method === 'PUT' ? '更新' : '创建'}文档,标题: ${title}`);
+            
+            const response = await fetch(apiUrl, {
+                method: method,
+                headers: { 
+                    'Content-Type': 'application/json',
+                    'X-Requested-With': 'XMLHttpRequest'
+                },
+                credentials: 'same-origin',
+                body: JSON.stringify({ 
+                    title, 
+                    content: processedContent,
+                    published: currentDocument && currentDocument.published ? currentDocument.published : false
+                })
+            });
+            
+            // 处理响应
+            if (response.status === 401) {
+                // 明确的未授权响应
+                console.warn('保存文档返回401未授权');
+                core.showAlert('未登录或会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            if (!response.ok) {
+                const errorText = await response.text();
+                let errorData;
+                try {
+                    errorData = JSON.parse(errorText);
+                } catch (e) {
+                    // 如果不是有效的JSON,直接使用文本
+                    throw new Error(errorText || '保存失败,请重试');
+                }
+                throw new Error(errorData.error || errorData.message || '保存失败,请重试');
+            }
+            
+            const savedDoc = await response.json();
+            console.log('保存的文档:', savedDoc);
+
+            // 确保savedDoc包含必要的时间字段
+            if (savedDoc) {
+                // 如果返回的保存文档中没有时间字段,从API获取完整文档信息
+                if (!savedDoc.createdAt || !savedDoc.updatedAt) {
+                    try {
+                        const docId = savedDoc.id || (currentDocument ? currentDocument.id : null);
+                        if (docId) {
+                            const docResponse = await fetch(`/api/documents/${docId}`, {
+                                headers: { 'Cache-Control': 'no-cache' },
+                                credentials: 'same-origin'
+                            });
+                            
+                            if (docResponse.ok) {
+                                const fullDoc = await docResponse.json();
+                                Object.assign(savedDoc, {
+                                    createdAt: fullDoc.createdAt,
+                                    updatedAt: fullDoc.updatedAt
+                                });
+                                console.log('获取到完整的文档时间信息:', fullDoc);
+                            }
+                        }
+                    } catch (timeError) {
+                        console.warn('获取文档完整时间信息失败:', timeError);
+                    }
+                }
+                
+                // 更新文档列表中的文档
+                const existingIndex = documents.findIndex(d => d.id === savedDoc.id);
+                if (existingIndex >= 0) {
+                    documents[existingIndex] = { ...documents[existingIndex], ...savedDoc };
+                } else {
+                    documents.push(savedDoc);
+                }
+            }
+
+            core.showAlert('文档保存成功', 'success');
+            this.hideEditor();
+            await this.loadDocuments(); // 重新加载文档列表
+        } catch (error) {
+            console.error('保存文档失败:', error);
+            core.showAlert('保存文档失败: ' + error.message, 'error');
+        } finally {
+            core.hideLoading();
+        }
+    },
+
+    // 渲染文档列表
+    renderDocumentList: function() {
+        const tbody = document.getElementById('documentTableBody');
+        tbody.innerHTML = '';
+        
+        if (documents.length === 0) {
+            tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">没有找到文档</td></tr>';
+            return;
+        }
+        
+        documents.forEach((doc, index) => {
+            // 确保文档时间有合理默认值
+            let createdAt = '未知';
+            let updatedAt = '未知';
+            
+            try {
+                // 尝试解析创建时间,如果失败则回退到默认值
+                if (doc.createdAt) {
+                    const createdDate = new Date(doc.createdAt);
+                    if (!isNaN(createdDate.getTime())) {
+                        createdAt = createdDate.toLocaleString('zh-CN', {
+                            year: 'numeric',
+                            month: '2-digit',
+                            day: '2-digit',
+                            hour: '2-digit',
+                            minute: '2-digit',
+                            second: '2-digit'
+                        });
+                    }
+                }
+                
+                // 尝试解析更新时间,如果失败则回退到默认值
+                if (doc.updatedAt) {
+                    const updatedDate = new Date(doc.updatedAt);
+                    if (!isNaN(updatedDate.getTime())) {
+                        updatedAt = updatedDate.toLocaleString('zh-CN', {
+                            year: 'numeric',
+                            month: '2-digit',
+                            day: '2-digit',
+                            hour: '2-digit',
+                            minute: '2-digit',
+                            second: '2-digit'
+                        });
+                    }
+                }
+                
+                // 简化时间显示逻辑 - 直接显示时间戳,不添加特殊标记
+                if (createdAt === '未知' && updatedAt !== '未知') {
+                    // 如果没有创建时间但有更新时间
+                    createdAt = '未记录';
+                } else if (updatedAt === '未知' && createdAt !== '未知') {
+                    // 如果没有更新时间但有创建时间
+                    updatedAt = '未更新';
+                }
+            } catch (error) {
+                console.warn(`解析文档时间失败:`, error, doc);
+            }
+            
+            const statusClasses = doc.published ? 'status-badge status-running' : 'status-badge status-stopped';
+            const statusText = doc.published ? '已发布' : '未发布';
+            
+            const row = document.createElement('tr');
+            row.innerHTML = `
+                <td>${index + 1}</td>
+                <td>${doc.title || '无标题文档'}</td>
+                <td>${createdAt}</td>
+                <td>${updatedAt}</td>
+                <td><span class="${statusClasses}">${statusText}</span></td>
+                <td class="action-buttons">
+                    <button class="action-btn edit-btn" title="编辑文档" onclick="documentManager.editDocument('${doc.id}')">
+                        <i class="fas fa-edit"></i>
+                    </button>
+                    <button class="action-btn ${doc.published ? 'unpublish-btn' : 'publish-btn'}" 
+                        title="${doc.published ? '取消发布' : '发布文档'}" 
+                        onclick="documentManager.togglePublish('${doc.id}')">
+                        <i class="fas ${doc.published ? 'fa-toggle-off' : 'fa-toggle-on'}"></i>
+                    </button>
+                    <button class="action-btn delete-btn" title="删除文档" onclick="documentManager.deleteDocument('${doc.id}')">
+                        <i class="fas fa-trash"></i>
+                    </button>
+                </td>
+            `;
+            
+            tbody.appendChild(row);
+        });
+    },
+
+    // 编辑文档
+    editDocument: async function(id) {
+        try {
+            console.log(`准备编辑文档,ID: ${id}`);
+            core.showLoading();
+            
+            // 检查会话状态,优化会话检查逻辑
+            let sessionValid = true;
+            try {
+                const sessionResponse = await fetch('/api/check-session', {
+                    headers: { 
+                        'Cache-Control': 'no-cache',
+                        'X-Requested-With': 'XMLHttpRequest' 
+                    },
+                    credentials: 'same-origin'
+                });
+                
+                if (sessionResponse.status === 401) {
+                    console.warn('会话已过期,无法编辑文档');
+                    sessionValid = false;
+                }
+            } catch (sessionError) {
+                console.warn('检查会话状态发生网络错误:', sessionError);
+                // 发生网络错误时不立即判定会话失效,继续尝试编辑操作
+            }
+            
+            // 只有在明确会话无效时才提示重新登录
+            if (!sessionValid) {
+                core.showAlert('您的会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            // 在本地查找文档
+            currentDocument = documents.find(doc => doc.id === id);
+            
+            // 如果本地未找到,从API获取
+            if (!currentDocument && id) {
+                try {
+                    console.log('从API获取文档详情');
+                    
+                    // 尝试多个可能的API路径
+                    const apiPaths = [
+                        `/api/documents/${id}`,
+                        `/api/documentation/${id}`
+                    ];
+                    
+                    let docResponse = null;
+                    let authError = false;
+                    
+                    for (const apiPath of apiPaths) {
+                        try {
+                            console.log(`尝试从 ${apiPath} 获取文档`);
+                            const response = await fetch(apiPath, {
+                                credentials: 'same-origin',
+                                headers: {
+                                    'X-Requested-With': 'XMLHttpRequest'
+                                }
+                            });
+                            
+                            // 只有明确401错误才认定为会话过期
+                            if (response.status === 401) {
+                                console.warn(`API ${apiPath} 返回401未授权`);
+                                authError = true;
+                                continue;
+                            }
+                            
+                            if (response.ok) {
+                                docResponse = response;
+                                console.log(`成功从 ${apiPath} 获取文档`);
+                                break;
+                            }
+                        } catch (pathError) {
+                            console.warn(`从 ${apiPath} 获取文档失败:`, pathError);
+                        }
+                    }
+                    
+                    // 处理认证错误
+                    if (authError && !docResponse) {
+                        core.showAlert('未登录或会话已过期,请重新登录', 'warning');
+                        setTimeout(() => {
+                            localStorage.removeItem('isLoggedIn');
+                            window.location.reload();
+                        }, 1500);
+                        return;
+                    }
+                    
+                    if (docResponse && docResponse.ok) {
+                        currentDocument = await docResponse.json();
+                        console.log('获取到文档详情:', currentDocument);
+                        
+                        // 确保文档包含必要的时间字段
+                        if (!currentDocument.createdAt && currentDocument.updatedAt) {
+                            // 如果没有创建时间但有更新时间,使用更新时间
+                            currentDocument.createdAt = currentDocument.updatedAt;
+                        } else if (!currentDocument.createdAt) {
+                            // 如果都没有,使用当前时间
+                            currentDocument.createdAt = new Date().toISOString();
+                        }
+                        
+                        if (!currentDocument.updatedAt) {
+                            // 如果没有更新时间,使用创建时间
+                            currentDocument.updatedAt = currentDocument.createdAt;
+                        }
+                        
+                        // 将获取到的文档添加到文档列表中
+                        const existingIndex = documents.findIndex(d => d.id === id);
+                        if (existingIndex >= 0) {
+                            documents[existingIndex] = currentDocument;
+                        } else {
+                            documents.push(currentDocument);
+                        }
+                    } else {
+                        throw new Error('所有API路径都无法获取文档');
+                    }
+                } catch (apiError) {
+                    console.error('从API获取文档详情失败:', apiError);
+                    core.showAlert('获取文档详情失败: ' + apiError.message, 'error');
+                }
+            }
+            
+            // 如果仍然没有找到文档,显示错误
+            if (!currentDocument) {
+                core.showAlert('未找到指定的文档', 'error');
+                return;
+            }
+            
+            // 显示编辑器界面并设置内容
+            this.showEditor();
+            
+            // 确保编辑器已初始化
+            if (!editorMd) {
+                await new Promise(resolve => setTimeout(resolve, 100));
+                this.initEditor();
+                await new Promise(resolve => setTimeout(resolve, 500));
+            }
+            
+            // 设置文档内容
+            if (editorMd) {
+                document.getElementById('documentTitle').value = currentDocument.title || '';
+                
+                if (currentDocument.content) {
+                    console.log(`设置文档内容,长度: ${currentDocument.content.length}`);
+                    editorMd.setMarkdown(currentDocument.content);
+                } else {
+                    console.log('文档内容为空,尝试额外获取内容');
+                    
+                    // 如果文档内容为空,尝试额外获取内容
+                    try {
+                        const contentResponse = await fetch(`/api/documents/${id}/content`, {
+                            credentials: 'same-origin',
+                            headers: {
+                                'X-Requested-With': 'XMLHttpRequest'
+                            }
+                        });
+                        
+                        // 只有明确401错误才提示重新登录
+                        if (contentResponse.status === 401) {
+                            core.showAlert('会话已过期,请重新登录', 'warning');
+                            setTimeout(() => {
+                                localStorage.removeItem('isLoggedIn');
+                                window.location.reload();
+                            }, 1500);
+                            return;
+                        }
+                        
+                        if (contentResponse.ok) {
+                            const contentData = await contentResponse.json();
+                            if (contentData.content) {
+                                currentDocument.content = contentData.content;
+                                editorMd.setMarkdown(contentData.content);
+                                console.log('成功获取额外内容');
+                            }
+                        }
+                    } catch (contentError) {
+                        console.warn('获取额外内容失败:', contentError);
+                    }
+                    
+                    // 如果仍然没有内容,设置为空
+                    if (!currentDocument.content) {
+                        editorMd.setMarkdown('');
+                    }
+                }
+            } else {
+                console.error('编辑器初始化失败,无法设置内容');
+                core.showAlert('编辑器初始化失败,请刷新页面重试', 'error');
+            }
+        } catch (error) {
+            console.error('编辑文档时出错:', error);
+            core.showAlert('编辑文档失败: ' + error.message, 'error');
+        } finally {
+            core.hideLoading();
+        }
+    },
+
+    // 查看文档
+    viewDocument: function(id) {
+        const doc = documents.find(doc => doc.id === id);
+        if (!doc) {
+            core.showAlert('未找到指定的文档', 'error');
+            return;
+        }
+
+        Swal.fire({
+            title: doc.title,
+            html: `<div class="document-preview">${marked.parse(doc.content || '')}</div>`,
+            width: '70%',
+            showCloseButton: true,
+            showConfirmButton: false,
+            customClass: {
+                container: 'document-preview-container',
+                popup: 'document-preview-popup',
+                content: 'document-preview-content'
+            }
+        });
+    },
+
+    // 删除文档
+    deleteDocument: async function(id) {
+        Swal.fire({
+            title: '确定要删除此文档吗?',
+            text: "此操作无法撤销!",
+            icon: 'warning',
+            showCancelButton: true,
+            confirmButtonColor: '#d33',
+            cancelButtonColor: '#3085d6',
+            confirmButtonText: '是,删除它',
+            cancelButtonText: '取消'
+        }).then(async (result) => {
+            if (result.isConfirmed) {
+                try {
+                    console.log(`尝试删除文档: ${id}`);
+                    
+                    // 检查会话状态
+                    const sessionResponse = await fetch('/api/check-session', {
+                        headers: { 'Cache-Control': 'no-cache' }
+                    });
+                    
+                    if (sessionResponse.status === 401) {
+                        // 会话已过期,提示用户并重定向到登录
+                        core.showAlert('您的会话已过期,请重新登录', 'warning');
+                        setTimeout(() => {
+                            localStorage.removeItem('isLoggedIn');
+                            window.location.reload();
+                        }, 1500);
+                        return;
+                    }
+                    
+                    // 使用正确的API路径删除
+                    const response = await fetch(`/api/documents/${id}`, { 
+                        method: 'DELETE',
+                        headers: {
+                            'Content-Type': 'application/json',
+                            'X-Requested-With': 'XMLHttpRequest'
+                        },
+                        credentials: 'same-origin' // 确保发送cookie
+                    });
+                    
+                    if (response.status === 401) {
+                        // 处理未授权错误
+                        core.showAlert('未登录或会话已过期,请重新登录', 'warning');
+                        setTimeout(() => {
+                            localStorage.removeItem('isLoggedIn');
+                            window.location.reload();
+                        }, 1500);
+                        return;
+                    }
+                    
+                    if (!response.ok) {
+                        const errorData = await response.json();
+                        throw new Error(errorData.error || '删除文档失败');
+                    }
+                    
+                    console.log('文档删除成功响应:', await response.json());
+                    core.showAlert('文档已成功删除', 'success');
+                    await this.loadDocuments(); // 重新加载文档列表
+                } catch (error) {
+                    console.error('删除文档失败:', error);
+                    core.showAlert('删除文档失败: ' + error.message, 'error');
+                }
+            }
+        });
+    },
+
+    // 切换文档发布状态
+    togglePublish: async function(id) {
+        try {
+            const doc = documents.find(d => d.id === id);
+            if (!doc) {
+                throw new Error('找不到指定文档');
+            }
+            
+            // 添加加载指示
+            core.showLoading();
+            
+            // 检查会话状态
+            const sessionResponse = await fetch('/api/check-session', {
+                headers: { 'Cache-Control': 'no-cache' }
+            });
+            
+            if (sessionResponse.status === 401) {
+                // 会话已过期,提示用户并重定向到登录
+                core.showAlert('您的会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            console.log(`尝试切换文档 ${id} 的发布状态,当前状态:`, doc.published);
+            
+            // 构建更新请求数据
+            const updateData = {
+                id: doc.id,
+                title: doc.title,
+                published: !doc.published // 切换发布状态
+            };
+            
+            // 使用正确的API端点进行更新
+            const response = await fetch(`/api/documentation/toggle-publish/${id}`, {
+                method: 'PUT',
+                headers: { 
+                    'Content-Type': 'application/json',
+                    'X-Requested-With': 'XMLHttpRequest'
+                },
+                credentials: 'same-origin', // 确保发送cookie
+                body: JSON.stringify(updateData)
+            });
+            
+            if (response.status === 401) {
+                // 处理未授权错误
+                core.showAlert('未登录或会话已过期,请重新登录', 'warning');
+                setTimeout(() => {
+                    localStorage.removeItem('isLoggedIn');
+                    window.location.reload();
+                }, 1500);
+                return;
+            }
+            
+            if (!response.ok) {
+                const errorData = await response.json();
+                throw new Error(errorData.error || '更新文档状态失败');
+            }
+            
+            // 更新成功
+            const updatedDoc = await response.json();
+            console.log('文档状态更新响应:', updatedDoc);
+            
+            // 更新本地文档列表
+            const docIndex = documents.findIndex(d => d.id === id);
+            if (docIndex >= 0) {
+                documents[docIndex].published = updatedDoc.published;
+                this.renderDocumentList();
+            }
+            
+            core.showAlert('文档状态已更新', 'success');
+        } catch (error) {
+            console.error('更改发布状态失败:', error);
+            core.showAlert('更改发布状态失败: ' + error.message, 'error');
+        } finally {
+            core.hideLoading();
+        }
+    }
+};
+
+// 全局公开文档管理模块
+window.documentManager = documentManager;
+
+/**
+ * 显示指定文档的内容
+ * @param {string} docId 文档ID
+ */
+async function showDocument(docId) {
+    try {
+        console.log('正在获取文档内容,ID:', docId);
+        
+        // 显示加载状态
+        const documentContent = document.getElementById('documentContent');
+        if (documentContent) {
+            documentContent.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档内容...</div>';
+        }
+        
+        // 获取文档内容
+        const response = await fetch(`/api/documentation/${docId}`);
+        if (!response.ok) {
+            throw new Error(`获取文档内容失败,状态码: ${response.status}`);
+        }
+        
+        const doc = await response.json();
+        console.log('获取到文档:', doc);
+        
+        // 更新文档内容区域
+        if (documentContent) {
+            if (doc.content) {
+                // 使用marked渲染markdown内容
+                documentContent.innerHTML = `
+                    <h1>${doc.title || '无标题'}</h1>
+                    ${doc.lastUpdated ? `<div class="doc-meta">最后更新: ${new Date(doc.lastUpdated).toLocaleDateString('zh-CN')}</div>` : ''}
+                    <div class="doc-content">${window.marked ? marked.parse(doc.content) : doc.content}</div>
+                `;
+            } else {
+                documentContent.innerHTML = `
+                    <h1>${doc.title || '无标题'}</h1>
+                    <div class="empty-content">
+                        <i class="fas fa-file-alt fa-3x"></i>
+                        <p>该文档暂无内容</p>
+                    </div>
+                `;
+            }
+        } else {
+            console.error('找不到文档内容容器,ID: documentContent');
+        }
+        
+        // 高亮当前选中的文档
+        highlightSelectedDocument(docId);
+    } catch (error) {
+        console.error('获取文档内容失败:', error);
+        
+        // 显示错误信息
+        const documentContent = document.getElementById('documentContent');
+        if (documentContent) {
+            documentContent.innerHTML = `
+                <div class="error-container">
+                    <i class="fas fa-exclamation-triangle fa-3x"></i>
+                    <h2>加载失败</h2>
+                    <p>无法获取文档内容: ${error.message}</p>
+                    <button class="btn btn-retry" onclick="showDocument('${docId}')">重试</button>
+                </div>
+            `;
+        }
+    }
+}
+
+/**
+ * 高亮选中的文档
+ * @param {string} docId 文档ID
+ */
+function highlightSelectedDocument(docId) {
+    // 移除所有高亮
+    const docLinks = document.querySelectorAll('.doc-list .doc-item');
+    docLinks.forEach(link => link.classList.remove('active'));
+    
+    // 添加当前高亮
+    const selectedLink = document.querySelector(`.doc-list .doc-item[data-id="${docId}"]`);
+    if (selectedLink) {
+        selectedLink.classList.add('active');
+        // 确保选中项可见
+        selectedLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
+    }
+}

+ 85 - 0
hubcmdui/web/js/error-handler.js

@@ -0,0 +1,85 @@
+// 客户端错误收集器
+
+(function() {
+  // 保存原始控制台方法
+  const originalConsoleError = console.error;
+  
+  // 重写console.error以捕获错误
+  console.error = function(...args) {
+    // 调用原始方法
+    originalConsoleError.apply(console, args);
+    
+    // 提取错误信息
+    const errorMessage = args.map(arg => {
+      if (arg instanceof Error) {
+        return arg.stack || arg.message;
+      } else if (typeof arg === 'object') {
+        try {
+          return JSON.stringify(arg);
+        } catch (e) {
+          return String(arg);
+        }
+      } else {
+        return String(arg);
+      }
+    }).join(' ');
+    
+    // 向服务器报告错误
+    reportErrorToServer({
+      message: errorMessage,
+      source: 'console.error',
+      type: 'console'
+    });
+  };
+  
+  // 全局错误处理
+  window.addEventListener('error', function(event) {
+    reportErrorToServer({
+      message: event.message,
+      source: event.filename,
+      lineno: event.lineno,
+      colno: event.colno,
+      stack: event.error ? event.error.stack : null,
+      type: 'uncaught'
+    });
+  });
+  
+  // Promise错误处理
+  window.addEventListener('unhandledrejection', function(event) {
+    const message = event.reason instanceof Error 
+      ? event.reason.message 
+      : String(event.reason);
+      
+    const stack = event.reason instanceof Error 
+      ? event.reason.stack 
+      : null;
+      
+    reportErrorToServer({
+      message: message,
+      stack: stack,
+      type: 'promise'
+    });
+  });
+  
+  // 向服务器发送错误报告
+  function reportErrorToServer(errorData) {
+    // 添加额外信息
+    const data = {
+      ...errorData,
+      userAgent: navigator.userAgent,
+      page: window.location.href,
+      timestamp: new Date().toISOString()
+    };
+    
+    // 发送错误报告到服务器
+    fetch('/api/client-error', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(data),
+      // 使用keepalive以确保在页面卸载时仍能发送
+      keepalive: true
+    }).catch(err => {
+      // 不记录这个错误,避免无限循环
+    });
+  }
+})();

+ 573 - 0
hubcmdui/web/js/menuManager.js

@@ -0,0 +1,573 @@
+/**
+ * 菜单管理模块 - 管理 data/config.json 中的 menuItems
+ */
+
+// 菜单项列表 (从 config.json 读取)
+let configMenuItems = [];
+let currentConfig = {}; // 保存当前完整的配置
+
+// 创建menuManager对象
+const menuManager = {
+    // 初始化菜单管理
+    init: async function() {
+        console.log('初始化菜单管理 (config.json)...');
+        this.renderMenuTableHeader(); // 渲染表头
+        await this.loadMenuItems();   // 加载菜单项
+        return Promise.resolve();
+    },
+
+    // 渲染菜单表格头部 (根据 config.json 结构调整)
+    renderMenuTableHeader: function() {
+        const menuTable = document.getElementById('menuTable');
+        if (!menuTable) return;
+
+        const thead = menuTable.querySelector('thead') || document.createElement('thead');
+        thead.innerHTML = `
+            <tr>
+                <th style="width: 5%">#</th>
+                <th style="width: 25%">文本 (Text)</th>
+                <th style="width: 40%">链接 (Link)</th>
+                <th style="width: 10%">新标签页 (New Tab)</th>
+                <th style="width: 20%">操作</th>
+            </tr>
+        `;
+
+        if (!menuTable.querySelector('thead')) {
+            menuTable.appendChild(thead);
+        }
+    },
+
+    // 加载菜单项 (从 /api/config 获取)
+    loadMenuItems: async function() {
+        try {
+            const menuTableBody = document.getElementById('menuTableBody');
+            if (menuTableBody) {
+                menuTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center;"><i class="fas fa-spinner fa-spin"></i> 正在加载菜单项...</td></tr>';
+            }
+
+            const response = await fetch('/api/config'); // 请求配置接口
+            if (!response.ok) {
+                throw new Error(`获取配置失败: ${response.statusText || response.status}`);
+            }
+
+            currentConfig = await response.json(); // 保存完整配置
+            configMenuItems = currentConfig.menuItems || []; // 提取菜单项,如果不存在则为空数组
+
+            this.renderMenuItems();
+            console.log('成功从 /api/config 加载菜单项', configMenuItems);
+
+        } catch (error) {
+            console.error('加载菜单项失败:', error);
+            const menuTableBody = document.getElementById('menuTableBody');
+            if (menuTableBody) {
+                menuTableBody.innerHTML = `
+                    <tr>
+                        <td colspan="5" style="text-align: center; color: #ff4d4f;">
+                            <i class="fas fa-exclamation-circle"></i>
+                            加载菜单项失败: ${error.message}
+                            <button onclick="menuManager.loadMenuItems()" class="retry-btn">
+                                <i class="fas fa-sync"></i> 重试
+                            </button>
+                        </td>
+                    </tr>
+                `;
+            }
+        }
+    },
+
+    // 渲染菜单项 (根据 config.json 结构)
+    renderMenuItems: function() {
+        const menuTableBody = document.getElementById('menuTableBody');
+        if (!menuTableBody) return;
+
+        menuTableBody.innerHTML = ''; // 清空现有内容
+
+        if (!Array.isArray(configMenuItems)) {
+            console.error("configMenuItems 不是一个数组:", configMenuItems);
+            menuTableBody.innerHTML = '<tr><td colspan="5" style="text-align: center; color: #ff4d4f;">菜单数据格式错误</td></tr>';
+            return;
+        }
+
+        configMenuItems.forEach((item, index) => {
+            const row = document.createElement('tr');
+            // 使用 index 作为临时 ID 进行操作
+            row.innerHTML = `
+                <td>${index + 1}</td>
+                <td>${item.text || ''}</td>
+                <td>${item.link || ''}</td>
+                <td>${item.newTab ? '是' : '否'}</td>
+                <td class="action-buttons">
+                    <button class="action-btn edit-btn" title="编辑菜单" onclick="menuManager.editMenuItem(${index})">
+                        <i class="fas fa-edit"></i>
+                    </button>
+                    <button class="action-btn delete-btn" title="删除菜单" onclick="menuManager.deleteMenuItem(${index})">
+                        <i class="fas fa-trash"></i>
+                    </button>
+                </td>
+            `;
+            menuTableBody.appendChild(row);
+        });
+    },
+
+    // 显示新菜单项行 (调整字段)
+    showNewMenuItemRow: function() {
+        const menuTableBody = document.getElementById('menuTableBody');
+        if (!menuTableBody) return;
+
+        const newRow = document.createElement('tr');
+        newRow.id = 'new-menu-item-row';
+        newRow.className = 'new-item-row';
+
+        newRow.innerHTML = `
+            <td>#</td>
+            <td><input type="text" id="new-text" placeholder="菜单文本"></td>
+            <td><input type="text" id="new-link" placeholder="链接地址"></td>
+            <td>
+                <select id="new-newTab">
+                    <option value="false">否</option>
+                    <option value="true">是</option>
+                </select>
+            </td>
+            <td>
+                <button class="action-btn" onclick="menuManager.saveNewMenuItem()">保存</button>
+                <button class="action-btn" onclick="menuManager.cancelNewMenuItem()">取消</button>
+            </td>
+        `;
+        menuTableBody.appendChild(newRow);
+        document.getElementById('new-text').focus();
+    },
+
+    // 保存新菜单项 (更新整个配置)
+    saveNewMenuItem: async function() {
+        try {
+            const text = document.getElementById('new-text').value.trim();
+            const link = document.getElementById('new-link').value.trim();
+            const newTab = document.getElementById('new-newTab').value === 'true';
+
+            if (!text || !link) {
+                throw new Error('文本和链接为必填项');
+            }
+
+            const newMenuItem = { text, link, newTab };
+
+            // 创建更新后的配置对象
+            const updatedConfig = {
+                ...currentConfig,
+                menuItems: [...(currentConfig.menuItems || []), newMenuItem] // 添加新项
+            };
+
+            // 调用API保存整个配置
+            const response = await fetch('/api/config', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify(updatedConfig) // 发送更新后的完整配置
+            });
+
+            if (!response.ok) {
+                 const errorData = await response.json();
+                throw new Error(`保存配置失败: ${errorData.details || response.statusText}`);
+            }
+
+            // 重新加载菜单项以更新视图和 currentConfig
+            await this.loadMenuItems();
+            this.cancelNewMenuItem(); // 移除编辑行
+            core.showAlert('菜单项已添加', 'success');
+
+        } catch (error) {
+            console.error('添加菜单项失败:', error);
+            core.showAlert('添加菜单项失败: ' + error.message, 'error');
+        }
+    },
+
+    // 取消新菜单项
+    cancelNewMenuItem: function() {
+        const newRow = document.getElementById('new-menu-item-row');
+        if (newRow) {
+            newRow.remove();
+        }
+    },
+
+    // 编辑菜单项 (使用 index 定位)
+    editMenuItem: function(index) {
+        const item = configMenuItems[index];
+        if (!item) {
+            core.showAlert('找不到指定的菜单项', 'error');
+            return;
+        }
+
+        Swal.fire({
+            title: '<div class="edit-title"><i class="fas fa-edit"></i> 编辑菜单项</div>',
+            html: `
+                <div class="edit-menu-form">
+                    <div class="form-group">
+                        <label for="edit-text">
+                            <i class="fas fa-font"></i> 菜单文本
+                        </label>
+                        <div class="input-wrapper">
+                            <input type="text" id="edit-text" class="modern-input" value="${item.text || ''}" placeholder="请输入菜单文本">
+                            <span class="input-icon"><i class="fas fa-heading"></i></span>
+                        </div>
+                        <small class="form-hint">菜单项显示的文本,保持简洁明了</small>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label for="edit-link">
+                            <i class="fas fa-link"></i> 链接地址
+                        </label>
+                        <div class="input-wrapper">
+                            <input type="text" id="edit-link" class="modern-input" value="${item.link || ''}" placeholder="请输入链接地址">
+                            <span class="input-icon"><i class="fas fa-globe"></i></span>
+                        </div>
+                        <small class="form-hint">完整URL路径(例如: https://example.com)或相对路径(例如: /docs)</small>
+                    </div>
+                    
+                    <div class="form-group toggle-switch">
+                        <label for="edit-newTab" class="toggle-label-text">
+                            <i class="fas fa-external-link-alt"></i> 在新标签页打开
+                        </label>
+                        <div class="toggle-switch-container">
+                            <input type="checkbox" id="edit-newTab" class="toggle-input" ${item.newTab ? 'checked' : ''}>
+                            <label for="edit-newTab" class="toggle-label"></label>
+                            <span class="toggle-status">${item.newTab ? '是' : '否'}</span>
+                        </div>
+                    </div>
+                    
+                    <div class="form-preview">
+                        <div class="preview-title"><i class="fas fa-eye"></i> 预览</div>
+                        <div class="preview-content">
+                            <a href="${item.link || '#'}" class="preview-link" target="${item.newTab ? '_blank' : '_self'}">
+                                <span class="preview-text">${item.text || '菜单项'}</span>
+                                ${item.newTab ? '<i class="fas fa-external-link-alt preview-icon"></i>' : ''}
+                            </a>
+                        </div>
+                    </div>
+                </div>
+                <style>
+                    .edit-title {
+                        font-size: 1.5rem;
+                        color: #3085d6;
+                        margin-bottom: 10px;
+                    }
+                    .edit-menu-form {
+                        text-align: left;
+                        padding: 0 15px;
+                    }
+                    .form-group {
+                        margin-bottom: 20px;
+                        position: relative;
+                    }
+                    .form-group label {
+                        display: block;
+                        margin-bottom: 8px;
+                        font-weight: 600;
+                        color: #444;
+                        font-size: 0.95rem;
+                    }
+                    .input-wrapper {
+                        position: relative;
+                    }
+                    .modern-input {
+                        width: 100%;
+                        padding: 12px 40px 12px 15px;
+                        border: 1px solid #ddd;
+                        border-radius: 8px;
+                        font-size: 1rem;
+                        transition: all 0.3s ease;
+                        box-shadow: 0 2px 5px rgba(0,0,0,0.05);
+                    }
+                    .modern-input:focus {
+                        border-color: #3085d6;
+                        box-shadow: 0 0 0 3px rgba(48, 133, 214, 0.2);
+                        outline: none;
+                    }
+                    .input-icon {
+                        position: absolute;
+                        right: 12px;
+                        top: 50%;
+                        transform: translateY(-50%);
+                        color: #aaa;
+                    }
+                    .form-hint {
+                        display: block;
+                        font-size: 0.8rem;
+                        color: #888;
+                        margin-top: 5px;
+                        font-style: italic;
+                    }
+                    .toggle-switch {
+                        display: flex;
+                        justify-content: space-between;
+                        align-items: center;
+                        padding: 5px 0;
+                    }
+                    .toggle-label-text {
+                        margin-bottom: 0 !important;
+                    }
+                    .toggle-switch-container {
+                        display: flex;
+                        align-items: center;
+                    }
+                    .toggle-input {
+                        display: none;
+                    }
+                    .toggle-label {
+                        display: block;
+                        width: 52px;
+                        height: 26px;
+                        background: #e6e6e6;
+                        border-radius: 13px;
+                        position: relative;
+                        cursor: pointer;
+                        transition: background 0.3s ease;
+                        box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
+                    }
+                    .toggle-label:after {
+                        content: '';
+                        position: absolute;
+                        top: 3px;
+                        left: 3px;
+                        width: 20px;
+                        height: 20px;
+                        background: white;
+                        border-radius: 50%;
+                        transition: transform 0.3s ease, box-shadow 0.3s ease;
+                        box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+                    }
+                    .toggle-input:checked + .toggle-label {
+                        background: #3085d6;
+                    }
+                    .toggle-input:checked + .toggle-label:after {
+                        transform: translateX(26px);
+                    }
+                    .toggle-status {
+                        margin-left: 10px;
+                        font-size: 0.9rem;
+                        color: #666;
+                        min-width: 20px;
+                    }
+                    .form-preview {
+                        margin-top: 25px;
+                        border: 1px dashed #ccc;
+                        border-radius: 8px;
+                        padding: 15px;
+                        background-color: #f9f9f9;
+                    }
+                    .preview-title {
+                        font-size: 0.9rem;
+                        color: #666;
+                        margin-bottom: 10px;
+                        text-align: center;
+                    }
+                    .preview-content {
+                        display: flex;
+                        justify-content: center;
+                        padding: 10px;
+                        background: white;
+                        border-radius: 6px;
+                        box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+                    }
+                    .preview-link {
+                        display: flex;
+                        align-items: center;
+                        color: #3085d6;
+                        text-decoration: none;
+                        font-weight: 500;
+                        padding: 5px 10px;
+                        border-radius: 4px;
+                        transition: background 0.2s ease;
+                    }
+                    .preview-link:hover {
+                        background: #f0f7ff;
+                    }
+                    .preview-text {
+                        margin-right: 5px;
+                    }
+                    .preview-icon {
+                        font-size: 0.8rem;
+                        opacity: 0.7;
+                    }
+                </style>
+            `,
+            showCancelButton: true,
+            confirmButtonText: '<i class="fas fa-save"></i> 保存',
+            cancelButtonText: '<i class="fas fa-times"></i> 取消',
+            confirmButtonColor: '#3085d6',
+            cancelButtonColor: '#6c757d',
+            width: '550px',
+            focusConfirm: false,
+            customClass: {
+                container: 'menu-edit-container',
+                popup: 'menu-edit-popup',
+                title: 'menu-edit-title',
+                confirmButton: 'menu-edit-confirm',
+                cancelButton: 'menu-edit-cancel'
+            },
+            didOpen: () => {
+                // 添加输入监听,更新预览
+                const textInput = document.getElementById('edit-text');
+                const linkInput = document.getElementById('edit-link');
+                const newTabToggle = document.getElementById('edit-newTab');
+                const toggleStatus = document.querySelector('.toggle-status');
+                const previewText = document.querySelector('.preview-text');
+                const previewLink = document.querySelector('.preview-link');
+                const previewIcon = document.querySelector('.preview-icon') || document.createElement('i');
+                
+                if (!previewIcon.classList.contains('fas')) {
+                    previewIcon.className = 'fas fa-external-link-alt preview-icon';
+                }
+                
+                const updatePreview = () => {
+                    previewText.textContent = textInput.value || '菜单项';
+                    previewLink.href = linkInput.value || '#';
+                    previewLink.target = newTabToggle.checked ? '_blank' : '_self';
+                    
+                    if (newTabToggle.checked) {
+                        if (!previewLink.contains(previewIcon)) {
+                            previewLink.appendChild(previewIcon);
+                        }
+                    } else {
+                        if (previewLink.contains(previewIcon)) {
+                            previewLink.removeChild(previewIcon);
+                        }
+                    }
+                };
+                
+                textInput.addEventListener('input', updatePreview);
+                linkInput.addEventListener('input', updatePreview);
+                newTabToggle.addEventListener('change', () => {
+                    toggleStatus.textContent = newTabToggle.checked ? '是' : '否';
+                    updatePreview();
+                });
+            },
+            preConfirm: () => {
+                const text = document.getElementById('edit-text').value.trim();
+                const link = document.getElementById('edit-link').value.trim();
+                const newTab = document.getElementById('edit-newTab').checked;
+                
+                if (!text) {
+                    Swal.showValidationMessage('<i class="fas fa-exclamation-circle"></i> 菜单文本不能为空');
+                    return false;
+                }
+                
+                if (!link) {
+                    Swal.showValidationMessage('<i class="fas fa-exclamation-circle"></i> 链接地址不能为空');
+                    return false;
+                }
+                
+                return { text, link, newTab };
+            }
+        }).then(async (result) => {
+            if (!result.isConfirmed) return;
+            
+            try {
+                // 显示保存中状态
+                Swal.fire({
+                    title: '保存中...',
+                    html: '<i class="fas fa-spinner fa-spin"></i> 正在保存菜单项',
+                    showConfirmButton: false,
+                    allowOutsideClick: false,
+                    willOpen: () => {
+                        Swal.showLoading();
+                    }
+                });
+                
+                // 更新配置中的菜单项
+                configMenuItems[index] = result.value;
+                
+                // 创建更新后的配置对象
+                const updatedConfig = {
+                    ...currentConfig,
+                    menuItems: configMenuItems // 更新后的菜单项数组
+                };
+                
+                // 调用API保存整个配置
+                const response = await fetch('/api/config', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify(updatedConfig) // 发送更新后的完整配置
+                });
+                
+                if (!response.ok) {
+                    const errorData = await response.json();
+                    throw new Error(`保存配置失败: ${errorData.details || response.statusText}`);
+                }
+                
+                // 重新渲染菜单项
+                this.renderMenuItems();
+
+                // 显示成功消息
+                Swal.fire({
+                    icon: 'success',
+                    title: '保存成功',
+                    html: '<i class="fas fa-check-circle"></i> 菜单项已更新',
+                    timer: 1500,
+                    showConfirmButton: false
+                });
+                
+            } catch (error) {
+                console.error('更新菜单项失败:', error);
+                Swal.fire({
+                    icon: 'error',
+                    title: '保存失败',
+                    html: `<i class="fas fa-times-circle"></i> 更新菜单项失败: ${error.message}`,
+                    confirmButtonText: '确定'
+                });
+            }
+        });
+    },
+
+    // 删除菜单项
+    deleteMenuItem: function(index) {
+        const item = configMenuItems[index];
+        if (!item) {
+            core.showAlert('找不到指定的菜单项', 'error');
+            return;
+        }
+        
+        Swal.fire({
+            title: '确认删除',
+            text: `确定要删除菜单项 "${item.text}" 吗?`,
+            icon: 'warning',
+            showCancelButton: true,
+            confirmButtonText: '删除',
+            cancelButtonText: '取消',
+            confirmButtonColor: '#d33'
+        }).then(async (result) => {
+            if (!result.isConfirmed) return;
+            
+            try {
+                // 从菜单项数组中移除指定项
+                configMenuItems.splice(index, 1);
+                
+                // 创建更新后的配置对象
+                const updatedConfig = {
+                    ...currentConfig,
+                    menuItems: configMenuItems // 更新后的菜单项数组
+                };
+                
+                // 调用API保存整个配置
+                const response = await fetch('/api/config', {
+                    method: 'POST',
+                    headers: { 'Content-Type': 'application/json' },
+                    body: JSON.stringify(updatedConfig) // 发送更新后的完整配置
+                });
+                
+                if (!response.ok) {
+                    const errorData = await response.json();
+                    throw new Error(`保存配置失败: ${errorData.details || response.statusText}`);
+                }
+                
+                // 重新渲染菜单项
+                this.renderMenuItems();
+                core.showAlert('菜单项已删除', 'success');
+                
+            } catch (error) {
+                console.error('删除菜单项失败:', error);
+                core.showAlert('删除菜单项失败: ' + error.message, 'error');
+            }
+        });
+    }
+};
+
+// 全局公开菜单管理模块
+window.menuManager = menuManager;

+ 93 - 0
hubcmdui/web/js/networkTest.js

@@ -0,0 +1,93 @@
+// 网络测试相关功能
+
+// 创建networkTest对象
+const networkTest = {
+    // 初始化函数
+    init: function() {
+        console.log('初始化网络测试模块...');
+        this.initNetworkTest();
+        return Promise.resolve();
+    },
+
+    // 初始化网络测试界面
+    initNetworkTest: function() {
+        const domainSelect = document.getElementById('domainSelect');
+        const testType = document.getElementById('testType');
+        
+        // 填充域名选择器
+        if (domainSelect) {
+            domainSelect.innerHTML = `
+                <option value="">选择预定义域名</option>
+                <option value="gcr.io">gcr.io</option>
+                <option value="ghcr.io">ghcr.io</option>
+                <option value="quay.io">quay.io</option>
+                <option value="k8s.gcr.io">k8s.gcr.io</option>
+                <option value="registry.k8s.io">registry.k8s.io</option>
+                <option value="mcr.microsoft.com">mcr.microsoft.com</option>
+                <option value="docker.elastic.co">docker.elastic.co</option>
+                <option value="registry-1.docker.io">registry-1.docker.io</option>
+            `;
+        }
+        
+        // 填充测试类型选择器
+        if (testType) {
+            testType.innerHTML = `
+                <option value="ping">Ping</option>
+                <option value="traceroute">Traceroute</option>
+            `;
+        }
+        
+        // 绑定测试按钮点击事件
+        const testButton = document.querySelector('#network-test button');
+        if (testButton) {
+            testButton.addEventListener('click', this.runNetworkTest);
+        }
+    },
+
+    // 运行网络测试
+    runNetworkTest: function() {
+        const domain = document.getElementById('domainSelect').value;
+        const testType = document.getElementById('testType').value;
+        const resultsDiv = document.getElementById('testResults');
+
+        // 验证选择了域名
+        if (!domain) {
+            core.showAlert('请选择目标域名', 'error');
+            return;
+        }
+
+        resultsDiv.innerHTML = '测试中,请稍候...';
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 60000); // 60秒超时
+
+        fetch('/api/network-test', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            body: JSON.stringify({ domain, type: testType }),
+            signal: controller.signal
+        })
+        .then(response => {
+            clearTimeout(timeoutId);
+            if (!response.ok) {
+                throw new Error('网络测试失败');
+            }
+            return response.text();
+        })
+        .then(result => {
+            resultsDiv.textContent = result;
+        })
+        .catch(error => {
+            console.error('网络测试出错:', error);
+            if (error.name === 'AbortError') {
+                resultsDiv.textContent = '测试超时,请稍后再试';
+            } else {
+                resultsDiv.textContent = '测试失败: ' + error.message;
+            }
+        });
+    }
+};
+
+// 全局公开网络测试模块
+window.networkTest = networkTest;

+ 1121 - 0
hubcmdui/web/js/systemStatus.js

@@ -0,0 +1,1121 @@
+/**
+ * 系统状态管理模块
+ */
+
+// --- 新增:用于缓存最新的系统状态数据 ---
+let currentSystemData = null;
+let DEBUG_MODE = false; // 控制是否输出调试信息
+
+// 简单的日志工具
+const logger = {
+    debug: function(...args) {
+        if (DEBUG_MODE) {
+            console.log('[systemStatus:DEBUG]', ...args);
+        }
+    },
+    log: function(...args) {
+        console.log('[systemStatus]', ...args);
+    },
+    warn: function(...args) {
+        console.warn('[systemStatus]', ...args);
+    },
+    error: function(...args) {
+        console.error('[systemStatus]', ...args);
+    },
+    // 开启或关闭调试模式
+    setDebug: function(enabled) {
+        DEBUG_MODE = !!enabled;
+        console.log(`[systemStatus] 调试模式已${DEBUG_MODE ? '开启' : '关闭'}`);
+    }
+};
+
+// 刷新系统状态
+async function refreshSystemStatus() {
+    logger.log('刷新系统状态...');
+    
+    showSystemStatusLoading(); // 显示表格/活动列表的加载状态
+    showDashboardLoading(); // 显示仪表盘卡片的加载状态
+    
+    try {
+        // 并行获取Docker状态和系统资源信息
+        logger.log('获取Docker状态和系统资源信息');
+        const [dockerResponse, resourcesResponse] = await Promise.all([
+            fetch('/api/docker/status').catch(err => { logger.error('Docker status fetch failed:', err); return null; }), // 添加 catch
+            fetch('/api/system-resources').catch(err => { logger.error('System resources fetch failed:', err); return null; }) // 添加 catch
+        ]);
+        
+        logger.debug('API响应结果:', { dockerOk: dockerResponse?.ok, resourcesOk: resourcesResponse?.ok });
+        
+        let dockerDataArray = null; // 用于存放容器数组
+        let isDockerServiceRunning = false; // 用于判断 Docker 服务本身是否响应
+        
+        if (dockerResponse && dockerResponse.ok) {
+            try {
+                // 假设 API 直接返回容器数组
+                dockerDataArray = await dockerResponse.json(); 
+                logger.debug('Docker数据:', JSON.stringify(dockerDataArray));
+                
+                // 只有当返回的是数组,且状态属性表明 Docker 正在运行时,才认为 Docker 服务是运行的
+                if (Array.isArray(dockerDataArray)) {
+                    // 检查特殊错误标记,这可能会在 dockerService.js 中添加
+                    const hasDockerUnavailableError = dockerDataArray.length === 1 && 
+                                                      dockerDataArray[0] && 
+                                                      dockerDataArray[0].error === 'DOCKER_UNAVAILABLE';
+                    
+                    const hasContainerListError = dockerDataArray.length === 1 && 
+                                                   dockerDataArray[0] && 
+                                                   dockerDataArray[0].error === 'CONTAINER_LIST_ERROR';
+
+                    // 只有在没有这两种特定错误时,才认为 Docker 服务正常
+                    isDockerServiceRunning = !hasDockerUnavailableError && !hasContainerListError;
+                    
+                    logger.debug(`Docker服务状态: ${isDockerServiceRunning ? '运行中' : '未运行'}, 错误状态:`, 
+                        { hasDockerUnavailableError, hasContainerListError });
+                } else {
+                    logger.warn('Docker数据不是数组:', typeof dockerDataArray);
+                    isDockerServiceRunning = false;
+                }
+            } catch (jsonError) {
+                logger.error('解析Docker数据失败:', jsonError);
+                dockerDataArray = []; // 解析失败视为空数组
+                isDockerServiceRunning = false; // JSON 解析失败,认为服务有问题
+            }
+        } else {
+            logger.warn('获取Docker状态失败');
+            dockerDataArray = []; // 请求失败视为空数组
+            isDockerServiceRunning = false; // 请求失败,认为服务未运行
+        }
+        
+        let resourcesData = null;
+        if (resourcesResponse && resourcesResponse.ok) {
+            try {
+                // --- 添加日志:打印原始响应文本 ---
+                const resourcesText = await resourcesResponse.text();
+                logger.debug('原始系统资源响应:', resourcesText);
+                resourcesData = JSON.parse(resourcesText); // 解析文本
+                logger.debug('解析后的系统资源数据:', resourcesData);
+            } catch (jsonError) {
+                logger.error('解析系统资源数据失败:', jsonError);
+                resourcesData = { cpu: null, memory: null, diskSpace: null }; 
+            }
+        } else {
+            logger.warn(`获取系统资源失败, 状态: ${resourcesResponse?.status}`);
+            resourcesData = { cpu: null, memory: null, diskSpace: null };
+        }
+        
+        // 合并数据
+        const combinedData = {
+            // 直接使用 isDockerServiceRunning 判断状态
+            dockerStatus: isDockerServiceRunning ? 'running' : 'stopped', 
+            // 直接使用获取到的容器数组
+            dockerContainers: Array.isArray(dockerDataArray) ? dockerDataArray : [], 
+            cpu: resourcesData?.cpu || { cores: 0, usage: undefined },
+            memory: resourcesData?.memory || { total: 0, free: 0, used: 0, usedPercentage: undefined },
+            disk: resourcesData?.disk || { size: '未知', used: '未知', available: '未知', percent: '未知' },
+            diskSpace: resourcesData?.diskSpace || { total: 0, free: 0, used: 0, usedPercentage: undefined },
+            // recentActivities 的逻辑保持不变,如果需要从 docker 数据生成,需要调整
+            // 暂时假设 recentActivities 来源于其他地方或保持为空
+             recentActivities: [] // 确保是空数组,除非有其他来源
+        };
+        
+        // --- 修改:将合并后的数据存入缓存 --- 
+        currentSystemData = combinedData; // 缓存数据
+        
+        logger.debug('合并后的状态数据:', currentSystemData);
+        updateSystemStatusUI(currentSystemData);
+        logger.log('系统状态加载完成');
+        
+        // 只在仪表盘页面(且登录框未显示时)才显示成功通知
+        const adminContainer = document.querySelector('.admin-container');
+        const loginModal = document.getElementById('loginModal');
+        const isDashboardVisible = adminContainer && window.getComputedStyle(adminContainer).display !== 'none';
+        const isLoginHidden = !loginModal || window.getComputedStyle(loginModal).display === 'none';
+        
+        if (isDashboardVisible && isLoginHidden) {
+            // 显示成功通知
+            Swal.fire({
+                icon: 'success',
+                title: '刷新成功',
+                text: '系统状态信息已更新',
+                toast: true,
+                position: 'top-end',
+                showConfirmButton: false,
+                timer: 3000
+            });
+        }
+    } catch (error) {
+        logger.error('刷新系统状态出错:', error);
+        showSystemStatusError(error.message);
+        showDashboardError(error.message);
+        
+        // 只在仪表盘页面(且登录框未显示时)才显示错误通知
+        const adminContainer = document.querySelector('.admin-container');
+        const loginModal = document.getElementById('loginModal');
+        const isDashboardVisible = adminContainer && window.getComputedStyle(adminContainer).display !== 'none';
+        const isLoginHidden = !loginModal || window.getComputedStyle(loginModal).display === 'none';
+        
+        if (isDashboardVisible && isLoginHidden) {
+            // 显示错误通知
+            Swal.fire({
+                icon: 'error',
+                title: '刷新失败',
+                text: error.message,
+                toast: true,
+                position: 'top-end',
+                showConfirmButton: false,
+                timer: 5000
+            });
+        }
+    }
+}
+
+// 显示系统状态错误
+function showSystemStatusError(message) {
+    console.error('系统状态错误:', message);
+    
+    // 更新Docker容器表格状态为错误
+    const containerBody = document.getElementById('dockerContainersBody');
+    if (containerBody) {
+        containerBody.innerHTML = `
+            <tr>
+                <td colspan="5" class="text-center">
+                    <div class="error-container">
+                        <i class="fas fa-exclamation-triangle text-danger fa-2x"></i>
+                        <p class="mt-2">无法加载Docker容器信息</p>
+                        <small class="text-muted">${message}</small>
+                    </div>
+                </td>
+            </tr>
+        `;
+    }
+    
+    // 更新活动表格状态为错误
+    const activitiesTable = document.getElementById('recentActivitiesTable');
+    if (activitiesTable) {
+        const tbody = activitiesTable.querySelector('tbody') || activitiesTable;
+        if (tbody) {
+            tbody.innerHTML = `
+                <tr>
+                    <td colspan="3" class="text-center">
+                        <div class="error-container">
+                            <i class="fas fa-exclamation-triangle text-danger fa-2x"></i>
+                            <p class="mt-2">无法加载系统活动信息</p>
+                            <small class="text-muted">${message}</small>
+                        </div>
+                    </td>
+                </tr>
+            `;
+        }
+    }
+}
+
+// 更新系统状态UI
+function updateSystemStatusUI(data) {
+    // 移除详细的数据日志
+    // console.log('[systemStatus] Updating UI with data:', data);
+    
+    if (!data) {
+        showSystemStatusError('无法获取系统状态数据');
+        showDashboardError('无法获取系统状态数据');
+        return;
+    }
+    
+    // 更新Docker状态指示器
+    updateDockerStatus(data.dockerStatus === 'running'); 
+    
+    // 更新仪表盘卡片
+    updateDashboardCards(data);
+    
+    // 更新活动列表 
+    updateActivitiesTable(Array.isArray(data.recentActivities) ? data.recentActivities : []);
+    
+    // --- 调用 dockerManager 来渲染容器表格 (确保调用) --- 
+    if (typeof dockerManager !== 'undefined' && dockerManager.renderContainersTable) {
+        dockerManager.renderContainersTable(data.dockerContainers, data.dockerStatus);
+    } else {
+        // 如果 dockerManager 不可用,提供一个简单错误信息
+        const containerBody = document.getElementById('dockerStatusTableBody');
+        if (containerBody) {
+            containerBody.innerHTML = `
+                <tr>
+                    <td colspan="5" class="text-center text-danger">
+                         <i class="fas fa-exclamation-triangle me-2"></i> 无法加载容器管理组件
+                    </td>
+                </tr>
+            `;
+        }
+    }
+    
+    // 更新存储使用进度条
+    try {
+        // 使用可选链和确保 parseDiskSpace 返回有效对象
+        const diskData = parseDiskSpace(data.diskSpace);
+        console.log('[systemStatus] Parsed disk data for progress bar:', diskData);
+        if (diskData && typeof diskData.usagePercent === 'number') { 
+            updateProgressBar('diskSpaceProgress', diskData.usagePercent);
+            console.log(`[systemStatus] Updated disk progress bar with: ${diskData.usagePercent}%`);
+        } else {
+            updateProgressBar('diskSpaceProgress', 0); // 出错或无数据时归零
+             console.log('[systemStatus] Disk data invalid or missing usagePercent for progress bar.');
+        }
+    } catch (e) {
+        console.warn('[systemStatus] Failed to update disk progress bar:', e);
+        updateProgressBar('diskSpaceProgress', 0); // 出错时归零
+    }
+    
+    // 更新内存使用进度条
+    try {
+        // 使用可选链
+        const memPercent = data.memory?.usedPercentage;
+        if (typeof memPercent === 'number') { 
+            updateProgressBar('memoryProgress', memPercent);
+            console.log(`[systemStatus] Updated memory progress bar with: ${memPercent}%`);
+        } else {
+            updateProgressBar('memoryProgress', 0);
+             console.log('[systemStatus] Memory data invalid or missing usedPercentage for progress bar.');
+        }
+    } catch (e) {
+        console.warn('[systemStatus] Failed to update memory progress bar:', e);
+        updateProgressBar('memoryProgress', 0);
+    }
+    
+    // 更新CPU使用进度条
+    try {
+        // 使用可选链
+        const cpuUsage = data.cpu?.usage;
+        if (typeof cpuUsage === 'number') { 
+            updateProgressBar('cpuProgress', cpuUsage);
+            console.log(`[systemStatus] Updated CPU progress bar with: ${cpuUsage}%`);
+        } else {
+            updateProgressBar('cpuProgress', 0);
+            console.log('[systemStatus] CPU data invalid or missing usage for progress bar.');
+        }
+    } catch (e) {
+        console.warn('[systemStatus] Failed to update CPU progress bar:', e);
+        updateProgressBar('cpuProgress', 0);
+    }
+    
+    console.log('[systemStatus] UI update process finished.');
+}
+
+// 显示系统状态加载
+function showSystemStatusLoading() {
+    console.log('显示系统状态加载中...');
+    
+    // 更新Docker容器表格状态为加载中
+    const containerTable = document.getElementById('dockerContainersTable');
+    const containerBody = document.getElementById('dockerContainersBody');
+    
+    if (containerTable && containerBody) {
+        containerBody.innerHTML = `
+            <tr>
+                <td colspan="5" class="text-center">
+                    <div class="loading-container">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="visually-hidden">加载中...</span>
+                        </div>
+                        <p class="mt-2">正在加载Docker容器信息...</p>
+                    </div>
+                </td>
+            </tr>
+        `;
+    }
+    
+    // 更新活动表格状态为加载中
+    const activitiesTable = document.getElementById('recentActivitiesTable');
+    if (activitiesTable) {
+        const tbody = activitiesTable.querySelector('tbody') || activitiesTable;
+        if (tbody) {
+            tbody.innerHTML = `
+                <tr>
+                    <td colspan="3" class="text-center">
+                        <div class="loading-container">
+                            <div class="spinner-border text-primary" role="status">
+                                <span class="visually-hidden">加载中...</span>
+                            </div>
+                            <p class="mt-2">正在加载系统活动信息...</p>
+                        </div>
+                    </td>
+                </tr>
+            `;
+        }
+    }
+}
+
+// 更新进度条
+function updateProgressBar(id, percentage) {
+    const bar = document.getElementById(id);
+    if (bar) {
+        // 确保 percentage 是有效的数字,否则设为 0
+        const validPercentage = (typeof percentage === 'number' && !isNaN(percentage)) ? Math.max(0, Math.min(100, percentage)) : 0;
+        console.log(`[systemStatus] Setting progress bar ${id} to ${validPercentage}%`);
+        bar.style.width = `${validPercentage}%`;
+        bar.setAttribute('aria-valuenow', validPercentage); // 更新 aria 属性
+        
+        // 根据使用率改变颜色
+        if (validPercentage < 50) {
+            bar.className = 'progress-bar bg-success';
+        } else if (validPercentage < 80) {
+            bar.className = 'progress-bar bg-warning';
+        } else {
+            bar.className = 'progress-bar bg-danger';
+        }
+    } else {
+         console.warn(`[systemStatus] Progress bar element with id '${id}' not found.`);
+    }
+}
+
+// 显示详情对话框 - 修改为直接使用缓存数据
+function showDetailsDialog(type) {
+    try {
+        let title = '';
+        let htmlContent = ''; // 用于生成内容的变量
+
+        if (!currentSystemData) {
+            htmlContent = '<div class="error-message">系统状态数据尚未加载。</div>';
+        } else {
+            switch(type) {
+                case 'containers':
+                    title = '容器详情';
+                    htmlContent = generateContainerDetailsHtml(currentSystemData.dockerContainers);
+                    break;
+                case 'memory':
+                    title = '内存使用详情';
+                    htmlContent = generateMemoryDetailsHtml(currentSystemData.memory);
+                    break;
+                case 'cpu':
+                    title = 'CPU 使用详情';
+                    htmlContent = generateCpuDetailsHtml(currentSystemData.cpu);
+                    break;
+                case 'disk':
+                    title = '磁盘使用详情';
+                    htmlContent = generateDiskDetailsHtml(currentSystemData.disk);
+                    break;
+                default:
+                    title = '详情';
+                    htmlContent = '<p>未知详情类型</p>';
+            }
+        }
+        
+        // 使用 SweetAlert2 显示对话框
+        Swal.fire({
+            title: title,
+            html: `<div id="detailsContent" class="details-swal-content">${htmlContent}</div>`,
+            width: '80%', // 可以更宽以容纳表格或详细信息
+            showConfirmButton: false,
+            showCloseButton: true,
+            customClass: {
+                popup: 'details-swal-popup' // 添加自定义类
+            }
+        });
+    } catch (error) {
+        console.error('显示详情对话框失败:', error);
+        core.showAlert('加载详情时出错: ' + error.message, 'error');
+    }
+}
+
+// --- 修改:生成容器详情 HTML (类Excel表头+多行数据) ---
+function generateContainerDetailsHtml(containers) {
+    if (!Array.isArray(containers)) return '<p class="text-muted text-center py-3">容器数据无效</p>';
+    
+    let html = '<div class="details-table-container">';
+    // 添加 resource-details-excel 类并使用 table-bordered
+    html += '<table class="table table-sm table-bordered table-hover details-table resource-details-excel">'; 
+    html += '<thead class="table-light"><tr><th>ID</th><th>名称</th><th>镜像</th><th>状态</th><th>创建时间</th></tr></thead>'; 
+    html += '<tbody>';
+    
+    if (containers.length > 0) {
+        containers.forEach(container => {
+            const statusClass = dockerManager.getContainerStatusClass(container.state); 
+            const containerId = container.id || '-';
+            const containerName = container.name || '-';
+            const containerImage = container.image || '-';
+            const containerState = container.state || '-';
+            const containerCreated = container.created ? formatTime(container.created) : '-';
+            
+            // 生成数据行,<td> 对应表头的顺序
+            html += `<tr>
+                <td title="${containerId}">${containerId.substring(0, 12)}</td>
+                <td title="${containerName}">${containerName}</td>
+                <td title="${containerImage}">${containerImage}</td>
+                <td><span class="badge ${statusClass}">${containerState}</span></td>
+                <td>${containerCreated}</td> 
+            </tr>`;
+        });
+    } else {
+        html += '<tr><td colspan="5" class="text-center text-muted py-3">没有找到容器</td></tr>'; // Colspan 调整为 5
+    }
+    
+    html += '</tbody></table></div>';
+    return html;
+}
+
+// --- 修改:生成内存详情 HTML (类Excel表头+单行数据) ---
+function generateMemoryDetailsHtml(memoryData) {
+    if (!memoryData) return '<p class="text-muted text-center py-3">内存数据不可用</p>';
+    
+    const total = formatByteSize(memoryData.total);
+    const used = formatByteSize(memoryData.used);
+    const free = formatByteSize(memoryData.free);
+    let usagePercent = '未知';
+    
+    if (typeof memoryData.percent === 'string') {
+        usagePercent = memoryData.percent;
+    } else if (typeof memoryData.total === 'number' && typeof memoryData.used === 'number' && memoryData.total > 0) {
+        const percent = (memoryData.used / memoryData.total) * 100;
+        usagePercent = `${percent.toFixed(1)}%`;
+    }
+    
+    let html = '<div class="details-table-container">';
+    html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
+    html += '<thead class="table-light"><tr><th>总内存</th><th>已用内存</th><th>空闲内存</th><th>内存使用率</th></tr></thead>';
+    html += `<tbody id="memory-details-body">`;
+    html += `<tr><td>${total}</td><td>${used}</td><td>${free}</td><td>${usagePercent}</td></tr>`;
+    html += '</tbody></table></div>';
+    return html;
+}
+
+// --- 修改:生成 CPU 详情 HTML (类Excel表头+单行数据) ---
+function generateCpuDetailsHtml(cpuData) {
+    if (!cpuData) return '<p class="text-muted text-center py-3">CPU 数据不可用</p>';
+    
+    let usagePercent = '未知';
+    if (Array.isArray(cpuData.loadAvg) && cpuData.loadAvg.length > 0) {
+        const cores = cpuData.cores || 1;
+        const cpuUsage = (cpuData.loadAvg[0] / cores) * 100;
+        usagePercent = `${Math.min(cpuUsage, 100).toFixed(1)}%`;
+    }
+    
+    // 处理CPU速度
+    let cpuSpeed = '未知';
+    if (cpuData.speed) {
+        cpuSpeed = `${cpuData.speed} MHz`;
+    }
+    
+    let html = '<div class="details-table-container">';
+    html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
+    html += '<thead class="table-light"><tr><th>CPU 核心数</th><th>CPU 型号</th><th>CPU 速度</th><th>当前使用率</th></tr></thead>';
+    html += `<tbody id="cpu-details-body">`;
+    html += `<tr><td>${cpuData.cores || '未知'}</td><td>${cpuData.model || '未知'}</td><td>${cpuSpeed}</td><td>${usagePercent}</td></tr>`;
+    html += '</tbody></table></div>';
+    
+    // 如果有loadAvg,添加额外的负载信息表格
+    if (Array.isArray(cpuData.loadAvg) && cpuData.loadAvg.length >= 3) {
+        html += '<h6 class="mt-3">系统平均负载</h6>';
+        html += '<table class="table table-sm table-bordered details-table">';
+        html += '<thead class="table-light"><tr><th>1分钟</th><th>5分钟</th><th>15分钟</th></tr></thead>';
+        html += '<tbody>';
+        html += `<tr><td>${cpuData.loadAvg[0].toFixed(2)}</td><td>${cpuData.loadAvg[1].toFixed(2)}</td><td>${cpuData.loadAvg[2].toFixed(2)}</td></tr>`;
+        html += '</tbody></table>';
+    }
+    
+    return html;
+}
+
+// --- 修改:生成磁盘详情 HTML (类Excel表头+单行数据) ---
+function generateDiskDetailsHtml(diskData) {
+    if (!diskData) return '<p class="text-muted text-center py-3">磁盘数据不可用</p>';
+    
+    // 确保有展示数据,无论是来自哪个来源
+    if (currentSystemData && currentSystemData.disk && !diskData.size) {
+        diskData = currentSystemData.disk;
+    }
+    
+    let html = '<div class="details-table-container">';
+    html += '<table class="table table-sm table-bordered details-table resource-details-excel">'; // 添加新类
+    html += '<thead class="table-light"><tr><th>总空间</th><th>已用空间</th><th>可用空间</th><th>使用率</th></tr></thead>';
+    html += `<tbody id="disk-details-body">`;
+    html += `<tr><td>${diskData.size || '未知'}</td><td>${diskData.used || '未知'}</td><td>${diskData.available || '未知'}</td><td>${diskData.percent || '未知'}</td></tr>`;
+    html += '</tbody></table></div>';
+    
+    // 添加额外的文件系统信息表格
+    if (diskData.filesystem) {
+        html += '<h6 class="mt-3">文件系统信息</h6>';
+        html += '<table class="table table-sm table-bordered details-table">';
+        html += '<tbody>';
+        html += `<tr><th>文件系统</th><td>${diskData.filesystem}</td></tr>`;
+        if (diskData.mounted) {
+            html += `<tr><th>挂载点</th><td>${diskData.mounted}</td></tr>`;
+        }
+        html += '</tbody></table>';
+    }
+    
+    return html;
+}
+
+// 更新Docker状态指示器
+function updateDockerStatus(available) {
+    console.log(`[systemStatus] Updating top Docker status indicator to: ${available ? 'running' : 'stopped'}`);
+    const statusIndicator = document.getElementById('dockerStatusIndicator'); // 假设这是顶部指示器的 ID
+    const statusText = document.getElementById('dockerStatusText'); // 假设这是顶部文本的 ID
+    
+    if (!statusIndicator || !statusText) {
+         console.warn('[systemStatus] Top Docker status indicator elements not found.');
+         return;
+    }
+    
+    if (available) {
+        statusIndicator.style.backgroundColor = 'var(--success-color, #4CAF50)'; // 使用 CSS 变量或默认值
+        statusText.textContent = 'Docker 运行中';
+        statusIndicator.title = 'Docker 服务正常运行中';
+        statusIndicator.classList.remove('stopped');
+        statusIndicator.classList.add('running');
+    } else {
+        statusIndicator.style.backgroundColor = 'var(--danger-color, #f44336)';
+        statusText.textContent = 'Docker 未运行';
+        statusIndicator.title = 'Docker 服务未运行或无法访问';
+        statusIndicator.classList.remove('running');
+        statusIndicator.classList.add('stopped');
+    }
+}
+
+// 显示Docker帮助信息
+function showDockerHelp() {
+    Swal.fire({
+        title: '<i class="fas fa-info-circle"></i> Docker 服务未运行',
+        html: `
+            <div class="docker-help-content text-start" style="text-align: left;">
+                <p class="mb-3">看起来 Docker 服务当前没有运行,或者应用程序无法连接到它。请检查以下常见原因:</p>
+                <ol class="mb-4" style="padding-left: 20px;">
+                    <li class="mb-2"><strong>服务未启动:</strong> 确保 Docker Desktop (Windows/Mac) 或 Docker daemon (Linux) 正在运行。</li>
+                    <li class="mb-2"><strong>权限问题:</strong> 运行此程序的用户可能需要添加到 'docker' 用户组 (Linux)。</li>
+                    <li class="mb-2"><strong>Docker Socket:</strong> 确认 Docker Socket 文件的路径和权限是否正确配置 (通常是 <code>/var/run/docker.sock</code> on Linux)。</li>
+                    <li class="mb-2"><strong>防火墙:</strong> 检查是否有防火墙规则阻止了与 Docker 的通信。</li>
+                </ol>
+                <div class="docker-cmd-examples">
+                    <p><strong>常用诊断命令 (Linux):</strong></p>
+                    <pre class="code-block"><code>sudo systemctl status docker</code></pre>
+                    <pre class="code-block"><code>sudo systemctl start docker</code></pre>
+                    <pre class="code-block"><code>sudo systemctl enable docker</code></pre>
+                    <pre class="code-block"><code>docker ps</code></pre>
+                    <pre class="code-block"><code>groups ${USER} # 检查是否在 docker 组</code></pre>
+                    <pre class="code-block mb-0"><code>sudo usermod -aG docker ${USER} # 添加用户到 docker 组 (需要重新登录生效)</code></pre>
+                </div>
+                 <p class="mt-3 small text-muted">如果问题仍然存在,请查阅 Docker 官方文档或检查应用程序的日志。</p>
+            </div>
+        `,
+        icon: null,
+        confirmButtonText: '我知道了',
+        customClass: {
+            popup: 'docker-help-popup',
+            content: 'docker-help-swal-content'
+        },
+        width: '650px'
+    });
+}
+
+// 初始化仪表板
+function initDashboard() {
+    // 检查仪表板容器是否存在
+    const dashboardGrid = document.querySelector('.dashboard-grid');
+    if (!dashboardGrid) return;
+    
+    // 清空现有内容
+    dashboardGrid.innerHTML = '';
+    
+    // 添加四个统计卡片
+    const cardsData = [
+        {
+            id: 'containers',
+            title: '容器数量',
+            icon: 'fa-cubes',
+            value: '--',
+            description: '运行中的容器总数',
+            trend: '',
+            action: '查看详情'
+        },
+        {
+            id: 'memory',
+            title: '内存使用',
+            icon: 'fa-memory',
+            value: '--',
+            description: '内存占用百分比',
+            trend: '',
+            action: '查看详情'
+        },
+        {
+            id: 'cpu',
+            title: 'CPU负载',
+            icon: 'fa-microchip',
+            value: '--',
+            description: 'CPU平均负载',
+            trend: '',
+            action: '查看详情'
+        },
+        {
+            id: 'disk',
+            title: '磁盘空间',
+            icon: 'fa-hdd',
+            value: '--',
+            description: '磁盘占用百分比',
+            trend: '',
+            action: '查看详情'
+        }
+    ];
+    
+    // 为每个卡片创建HTML并添加到仪表板网格
+    cardsData.forEach(card => {
+        const cardElement = createDashboardCard(card);
+        dashboardGrid.appendChild(cardElement);
+    });
+    
+    // 初始化刷新按钮
+    const refreshBtn = document.getElementById('refreshSystemBtn');
+    if (refreshBtn) {
+        refreshBtn.addEventListener('click', refreshSystemStatus);
+    }
+    
+    // 初始加载系统状态
+    refreshSystemStatus(); // <-- 调用确保加载数据
+    
+    console.log('仪表板初始化完成');
+}
+
+// 创建仪表板卡片
+function createDashboardCard(data) {
+    const card = document.createElement('div');
+    card.className = 'dashboard-card';
+    card.id = `${data.id}-card`;
+    
+    card.innerHTML = `
+        <div class="card-icon">
+            <i class="fas ${data.icon}"></i>
+        </div>
+        <h3 class="card-title">${data.title}</h3>
+        <div class="card-value" id="${data.id}-value">${data.value}</div>
+        <div class="card-description">${data.description}</div>
+        <div class="card-footer">
+            <div class="trend ${data.trend}" id="${data.id}-trend"></div>
+            <div class="card-action" onclick="systemStatus.showDetailsDialog('${data.id}')">${data.action}</div>
+        </div>
+    `;
+    
+    return card;
+}
+
+// 更新仪表板卡片
+function updateDashboardCards(data) {
+    logger.debug('更新仪表板卡片:', data);
+    
+    if (!data) {
+        logger.error('仪表板数据为空');
+        return;
+    }
+    
+    // 更新容器数量卡片
+    const containersValue = document.getElementById('containers-value');
+    if (containersValue) {
+        // 确保 dockerContainers 是数组
+        containersValue.textContent = Array.isArray(data?.dockerContainers) ? data.dockerContainers.length : '--'; 
+        logger.debug(`容器数量卡片更新为: ${containersValue.textContent}`);
+    }
+    
+    // 更新内存使用卡片
+    const memoryValue = document.getElementById('memory-value');
+    if (memoryValue) {
+        let memPercent = null;
+        
+        logger.debug(`内存数据:`, data.memory);
+        
+        // 特别处理兼容层返回的数据格式
+        if (data.memory && typeof data.memory === 'object') {
+            // 首先检查是否有 percent 属性
+            if (typeof data.memory.percent === 'string') {
+                memPercent = parseFloat(data.memory.percent.replace('%', ''));
+            } 
+            // 如果 percent 不存在或无效,尝试从 total 和 used 计算
+            else if (typeof data.memory.total === 'number' && typeof data.memory.used === 'number' && data.memory.total > 0) {
+                memPercent = (data.memory.used / data.memory.total) * 100;
+            }
+            // 将单位转换为更易读的格式
+            const totalMemory = formatByteSize(data.memory.total);
+            const usedMemory = formatByteSize(data.memory.used);
+            const freeMemory = formatByteSize(data.memory.free);
+            
+            // 更新内存详情表格中的值
+            updateMemoryDetailsTable(totalMemory, usedMemory, freeMemory, memPercent);
+        }
+        
+        memoryValue.textContent = (typeof memPercent === 'number' && !isNaN(memPercent)) 
+            ? `${memPercent.toFixed(1)}%` // 保留一位小数
+            : '未知'; 
+        logger.debug(`内存卡片更新为: ${memoryValue.textContent}`);
+        
+        // 更新内存进度条
+        const memoryProgressBar = document.getElementById('memory-progress');
+        if (memoryProgressBar && typeof memPercent === 'number' && !isNaN(memPercent)) {
+            memoryProgressBar.style.width = `${memPercent}%`;
+            if (memPercent > 90) {
+                memoryProgressBar.className = 'progress-bar bg-danger';
+            } else if (memPercent > 70) {
+                memoryProgressBar.className = 'progress-bar bg-warning';
+            } else {
+                memoryProgressBar.className = 'progress-bar bg-success';
+            }
+        }
+    }
+    
+    // 更新CPU负载卡片
+    const cpuValue = document.getElementById('cpu-value');
+    if (cpuValue) {
+        let cpuUsage = null;
+        
+        // 特别处理兼容层返回的CPU数据
+        if (data.cpu && typeof data.cpu === 'object') {
+            logger.debug(`CPU数据:`, data.cpu);
+            
+            // 如果有loadAvg数组,使用第一个值(1分钟平均负载)
+            if (Array.isArray(data.cpu.loadAvg) && data.cpu.loadAvg.length > 0) {
+                // 负载需要除以核心数来计算百分比
+                const cores = data.cpu.cores || 1;
+                cpuUsage = (data.cpu.loadAvg[0] / cores) * 100;
+                // 防止超过100%
+                cpuUsage = Math.min(cpuUsage, 100);
+                
+                // 更新CPU详情表格
+                updateCpuDetailsTable(data.cpu.cores, data.cpu.model, data.cpu.speed, cpuUsage);
+            }
+        }
+        
+        cpuValue.textContent = (typeof cpuUsage === 'number' && !isNaN(cpuUsage)) 
+             ? `${cpuUsage.toFixed(1)}%` // 保留一位小数
+             : '未知';
+        logger.debug(`CPU卡片更新为: ${cpuValue.textContent}`);
+        
+        // 更新CPU进度条
+        const cpuProgressBar = document.getElementById('cpu-progress');
+        if (cpuProgressBar && typeof cpuUsage === 'number' && !isNaN(cpuUsage)) {
+            cpuProgressBar.style.width = `${cpuUsage}%`;
+            if (cpuUsage > 90) {
+                cpuProgressBar.className = 'progress-bar bg-danger';
+            } else if (cpuUsage > 70) {
+                cpuProgressBar.className = 'progress-bar bg-warning';
+            } else {
+                cpuProgressBar.className = 'progress-bar bg-success';
+            }
+        }
+    }
+    
+    // 更新磁盘空间卡片
+    const diskValue = document.getElementById('disk-value');
+    const diskTrend = document.getElementById('disk-trend');
+    if (diskValue) {
+        let diskPercent = null;
+        
+        if (data.disk && typeof data.disk === 'object') {
+            logger.debug('磁盘数据:', data.disk); 
+            
+            // 特别处理直接从df命令返回的格式
+            if (data.disk.percent) {
+                // 如果percent属性存在,直接使用
+                if (typeof data.disk.percent === 'string') {
+                    diskPercent = parseFloat(data.disk.percent.replace('%', ''));
+                } else if (typeof data.disk.percent === 'number') {
+                    diskPercent = data.disk.percent;
+                }
+                
+                // 更新磁盘详情表格
+                updateDiskDetailsTable(
+                    data.disk.size || "未知", 
+                    data.disk.used || "未知", 
+                    data.disk.available || "未知", 
+                    diskPercent
+                );
+            }
+        } else if (data.diskSpace && typeof data.diskSpace === 'object') {
+            logger.debug('使用旧磁盘数据格式:', data.diskSpace); 
+            
+            // 兼容老的diskSpace字段
+            if (data.diskSpace.percent) {
+                if (typeof data.diskSpace.percent === 'string') {
+                    diskPercent = parseFloat(data.diskSpace.percent.replace('%', ''));
+                } else if (typeof data.diskSpace.percent === 'number') {
+                    diskPercent = data.diskSpace.percent;
+                }
+                
+                // 更新磁盘详情表格
+                updateDiskDetailsTable(
+                    data.diskSpace.size || "未知", 
+                    data.diskSpace.used || "未知", 
+                    data.diskSpace.available || "未知", 
+                    diskPercent
+                );
+            }
+        }
+
+        if (typeof diskPercent === 'number' && !isNaN(diskPercent)) {
+            diskValue.textContent = `${diskPercent.toFixed(0)}%`; // 磁盘百分比通常不带小数
+            logger.debug(`磁盘卡片更新为: ${diskValue.textContent}`);
+            
+            // 更新磁盘进度条
+            const diskProgressBar = document.getElementById('disk-progress');
+            if (diskProgressBar) {
+                diskProgressBar.style.width = `${diskPercent}%`;
+                if (diskPercent > 90) {
+                    diskProgressBar.className = 'progress-bar bg-danger';
+                } else if (diskPercent > 70) {
+                    diskProgressBar.className = 'progress-bar bg-warning';
+                } else {
+                    diskProgressBar.className = 'progress-bar bg-success';
+                }
+            }
+            
+            // 更新趋势信息
+            if (diskTrend) {
+                 if (diskPercent > 90) {
+                    diskTrend.className = 'trend down text-danger'; 
+                    diskTrend.innerHTML = '<i class="fas fa-exclamation-triangle"></i> 磁盘空间不足';
+                } else if (diskPercent > 75) {
+                    diskTrend.className = 'trend down text-warning'; 
+                    diskTrend.innerHTML = '<i class="fas fa-arrow-up"></i> 磁盘使用率较高';
+                } else {
+                    diskTrend.className = 'trend up text-success'; 
+                    diskTrend.innerHTML = '<i class="fas fa-check-circle"></i> 磁盘空间充足';
+                }
+            }
+        } else {
+            diskValue.textContent = '未知'; 
+            if(diskTrend) diskTrend.innerHTML = ''; 
+            logger.debug(`磁盘卡片值为未知,无效百分比: ${diskPercent}`);
+        }
+    }
+}
+
+// 格式化字节大小为易读格式
+function formatByteSize(bytes) {
+    if (bytes === undefined || bytes === null) return '未知';
+    if (typeof bytes === 'string') {
+        if (!isNaN(parseInt(bytes))) {
+            bytes = parseInt(bytes);
+        } else {
+            return bytes; // 如果已经是格式化字符串,直接返回
+        }
+    }
+    
+    if (typeof bytes !== 'number' || isNaN(bytes)) return '未知';
+    
+    const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
+    let size = bytes;
+    let unitIndex = 0;
+    
+    while (size >= 1024 && unitIndex < units.length - 1) {
+        size /= 1024;
+        unitIndex++;
+    }
+    
+    return `${size.toFixed(2)} ${units[unitIndex]}`;
+}
+
+// 更新内存详情表格
+function updateMemoryDetailsTable(total, used, free, percent) {
+    const memDetailsBody = document.getElementById('memory-details-body');
+    if (memDetailsBody) {
+        const percentText = typeof percent === 'number' ? `${percent.toFixed(1)}%` : '未知';
+        memDetailsBody.innerHTML = `<tr><td>${total}</td><td>${used}</td><td>${free}</td><td>${percentText}</td></tr>`;
+    }
+}
+
+// 更新CPU详情表格
+function updateCpuDetailsTable(cores, model, speed, usage) {
+    const cpuDetailsBody = document.getElementById('cpu-details-body');
+    if (cpuDetailsBody) {
+        const usageText = typeof usage === 'number' ? `${usage.toFixed(1)}%` : '未知';
+        let speedText = speed;
+        if (typeof speed === 'number') {
+            speedText = `${speed} MHz`;
+        }
+        cpuDetailsBody.innerHTML = `<tr><td>${cores || '未知'}</td><td>${model || '未知'}</td><td>${speedText}</td><td>${usageText}</td></tr>`;
+    }
+}
+
+// 更新磁盘详情表格
+function updateDiskDetailsTable(total, used, available, percent) {
+    const diskDetailsBody = document.getElementById('disk-details-body');
+    if (diskDetailsBody) {
+        const percentText = typeof percent === 'number' ? `${percent.toFixed(0)}%` : '未知';
+        diskDetailsBody.innerHTML = `<tr><td>${total}</td><td>${used}</td><td>${available}</td><td>${percentText}</td></tr>`;
+    }
+}
+
+// 更新活动表格
+function updateActivitiesTable(activities) {
+    const table = document.getElementById('recentActivitiesTable');
+    if (!table) {
+         console.warn('[systemStatus] Recent activities table not found.');
+         return;
+    }
+    
+    // 获取 tbody,如果不存在则创建
+    let tbody = table.querySelector('tbody');
+    if (!tbody) {
+        tbody = document.createElement('tbody');
+        table.appendChild(tbody);
+    }
+    
+    // 清空表格内容
+    tbody.innerHTML = '';
+    
+    // 确保 activities 是数组
+    if (!Array.isArray(activities)) {
+         console.warn('[systemStatus] activities data is not an array:', activities);
+         activities = []; // 设为空数组避免错误
+    }
+
+    if (activities.length === 0) {
+        tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted py-3"><i class="fas fa-info-circle me-2"></i>暂无活动记录</td></tr>';
+    } else {
+        let html = '';
+        activities.forEach(activity => {
+            // 添加简单的 HTML 转义以防止 XSS (更健壮的方案应使用库)
+            const safeDetails = (activity.details || '').replace(/</g, "&lt;").replace(/>/g, "&gt;");
+            html += `
+                <tr>
+                    <td data-label="时间">${formatTime(activity.timestamp)}</td>
+                    <td data-label="事件">${activity.event || '未知事件'}</td>
+                    <td data-label="详情" title="${safeDetails}" class="text-truncate">${safeDetails}</td>
+                </tr>
+            `;
+        });
+        tbody.innerHTML = html;
+    }
+    console.log(`[systemStatus] Updated activities table with ${activities.length} items.`);
+}
+
+// 格式化时间
+function formatTime(timestamp) {
+    if (!timestamp) return '未知时间';
+    
+    const date = new Date(timestamp);
+    const now = new Date();
+    
+    // 如果是今天的时间,只显示小时和分钟
+    if (date.toDateString() === now.toDateString()) {
+        return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
+    }
+    
+    // 否则显示完整日期和时间
+    return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
+}
+
+// 显示仪表盘加载状态
+function showDashboardLoading() {
+    const cards = ['containers', 'memory', 'cpu', 'disk'];
+    cards.forEach(id => {
+        const valueElement = document.getElementById(`${id}-value`);
+        if (valueElement) {
+            valueElement.innerHTML = '<div class="loading-spinner-small"></div>';
+        }
+    });
+}
+
+// 显示仪表盘错误
+function showDashboardError(message) {
+    // 在仪表盘上方添加错误通知
+    const dashboardGrid = document.querySelector('.dashboard-grid');
+    if (dashboardGrid) {
+        // 检查是否已存在错误通知,避免重复添加
+        let errorNotice = document.getElementById('dashboard-error-notice');
+        if (!errorNotice) {
+            errorNotice = document.createElement('div');
+            errorNotice.id = 'dashboard-error-notice';
+            errorNotice.className = 'dashboard-error-notice';
+            errorNotice.innerHTML = `
+                <i class="fas fa-exclamation-circle"></i>
+                <span>数据加载失败: ${message}</span>
+                <button onclick="systemStatus.refreshSystemStatus()">重试</button>
+            `;
+            dashboardGrid.parentNode.insertBefore(errorNotice, dashboardGrid);
+            
+            // 5秒后自动隐藏错误通知
+            setTimeout(() => {
+                if (errorNotice.parentNode) {
+                    errorNotice.classList.add('fade-out');
+                    setTimeout(() => {
+                        if (errorNotice.parentNode) {
+                            errorNotice.parentNode.removeChild(errorNotice);
+                        }
+                    }, 500);
+                }
+            }, 5000);
+        }
+    }
+}
+
+// --- 明确将需要全局调用的函数暴露到 window.systemStatus ---
+// (确保 systemStatus 对象存在)
+window.systemStatus = window.systemStatus || {}; 
+
+// 暴露刷新函数(可能被其他模块或 HTML 调用)
+window.systemStatus.refreshSystemStatus = refreshSystemStatus;
+
+// 暴露显示详情函数(被 HTML 调用)
+window.systemStatus.showDetailsDialog = showDetailsDialog;
+
+// 暴露显示 Docker 帮助函数(被 HTML 或 dockerManager 调用)
+window.systemStatus.showDockerHelp = showDockerHelp;
+
+// 暴露初始化仪表盘函数(可能被 app.js 调用)
+window.systemStatus.initDashboard = initDashboard;
+
+// 暴露调试设置函数,方便开发时打开调试
+window.systemStatus.setDebug = logger.setDebug;
+
+/* 添加一些基础样式到 CSS (如果 web/style.css 不可用,这里会失败) */
+/* 理想情况下,这些样式应该放在 web/style.css */
+const customHelpStyles = `
+.docker-help-popup .swal2-title {
+    font-size: 1.5rem;
+    color: var(--primary-color);
+    margin-bottom: 1.5rem;
+}
+/* 强制 SweetAlert 内容区左对齐 */
+.docker-help-popup .swal2-html-container {
+    text-align: left !important;
+    margin-left: 1rem; /* 可选:增加左边距 */
+    margin-right: 1rem; /* 可选:增加右边距 */
+    /* border: 1px solid red !important; /* 临时调试边框 */
+}
+/* 确保我们自己的内容容器也强制左对齐 */
+.docker-help-popup .docker-help-content {
+    text-align: left !important;
+}
+.docker-help-swal-content {
+    /* 这个类可能不再需要,但保留以防万一 */
+    font-size: 0.95rem;
+    line-height: 1.6;
+}
+.docker-help-content ol {
+    margin-left: 1rem; /* 确保列表相对于左对齐的内容有缩进 */
+}
+/* 确保列表和代码块也继承或设置为左对齐 */
+.docker-help-popup .docker-help-content ol,
+.docker-help-popup .docker-help-content pre {
+    text-align: left !important;
+}
+.docker-help-content .code-block {
+    background-color: #f8f9fa; 
+    border: 1px solid #e9ecef;
+    padding: 0.5rem 0.8rem;
+    border-radius: var(--radius-sm);
+    font-family: var(--font-mono);
+    font-size: 0.85rem;
+    margin-bottom: 0.5rem;
+    white-space: pre-wrap; /* 允许换行 */
+    word-wrap: break-word; /* 强制换行 */
+}
+.docker-help-content .code-block code {
+    background: none;
+    padding: 0;
+    color: inherit;
+}
+`;
+
+// 尝试将样式添加到页面 (这是一种不太优雅的方式,最好是在 CSS 文件中定义)
+try {
+    const styleSheet = document.createElement("style");
+    styleSheet.type = "text/css";
+    styleSheet.innerText = customHelpStyles;
+    document.head.appendChild(styleSheet);
+} catch (e) {
+    console.warn('无法动态添加 Docker 帮助样式:', e);
+}

+ 260 - 0
hubcmdui/web/js/userCenter.js

@@ -0,0 +1,260 @@
+/**
+ * 用户中心管理模块
+ */
+
+// 获取用户信息
+async function getUserInfo() {
+    try {
+        // 先检查是否已登录
+        const sessionResponse = await fetch('/api/check-session');
+        const sessionData = await sessionResponse.json();
+        
+        if (!sessionData.authenticated) {
+            // 用户未登录,不显示错误,静默返回
+            console.log('用户未登录或会话无效,跳过获取用户信息');
+            return;
+        }
+        
+        // 用户已登录,获取用户信息
+        console.log('会话有效,尝试获取用户信息...');
+        const response = await fetch('/api/user-info');
+        if (!response.ok) {
+            // 检查是否是认证问题
+            if (response.status === 401) {
+                console.log('会话已过期,需要重新登录');
+                return;
+            }
+            throw new Error('获取用户信息失败');
+        }
+        
+        const data = await response.json();
+        console.log('获取到用户信息:', data);
+        
+        // 更新顶部导航栏的用户名
+        const currentUsername = document.getElementById('currentUsername');
+        if (currentUsername) {
+            currentUsername.textContent = data.username || '未知用户';
+        }
+        
+        // 更新统计卡片数据
+        const loginCountElement = document.getElementById('loginCount');
+        if (loginCountElement) {
+            loginCountElement.textContent = data.loginCount || '0';
+        }
+        
+        const accountAgeElement = document.getElementById('accountAge');
+        if (accountAgeElement) {
+            accountAgeElement.textContent = data.accountAge ? `${data.accountAge}天` : '0天';
+        }
+        
+        const lastLoginElement = document.getElementById('lastLogin');
+        if (lastLoginElement) {
+            let lastLogin = data.lastLogin || '未知';
+            // 检查是否包含 "今天" 字样,添加样式
+            if (lastLogin.includes('今天')) {
+                lastLoginElement.innerHTML = `<span class="today-login">${lastLogin}</span>`;
+            } else {
+                lastLoginElement.textContent = lastLogin;
+            }
+        }
+    } catch (error) {
+        console.error('获取用户信息失败:', error);
+        // 不显示错误通知,只在控制台记录错误
+    }
+}
+
+// 修改密码
+async function changePassword(event) {
+    if (event) {
+        event.preventDefault();
+    }
+    
+    const form = document.getElementById('changePasswordForm');
+    const currentPassword = form.querySelector('#ucCurrentPassword').value;
+    const newPassword = form.querySelector('#ucNewPassword').value;
+    const confirmPassword = form.querySelector('#ucConfirmPassword').value;
+    
+    // 验证表单
+    if (!currentPassword || !newPassword || !confirmPassword) {
+        return core.showAlert('所有字段都不能为空', 'error');
+    }
+    
+    if (newPassword !== confirmPassword) {
+        return core.showAlert('两次输入的新密码不一致', 'error');
+    }
+    
+    // 密码复杂度检查
+    if (!isPasswordComplex(newPassword)) {
+        return core.showAlert('密码必须包含至少1个字母、1个数字和1个特殊字符,长度在8-16位之间', 'error');
+    }
+    
+    // 显示加载状态
+    const submitButton = form.querySelector('button[type="submit"]');
+    const originalButtonText = submitButton.innerHTML;
+    submitButton.disabled = true;
+    submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
+    
+    try {
+        const response = await fetch('/api/change-password', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                currentPassword,
+                newPassword
+            })
+        });
+        
+        // 无论成功与否,去除加载状态
+        submitButton.disabled = false;
+        submitButton.innerHTML = originalButtonText;
+        
+        if (!response.ok) {
+            const errorData = await response.json();
+            throw new Error(errorData.error || '修改密码失败');
+        }
+        
+        // 清空表单
+        form.reset();
+        
+        // 设置倒计时并显示
+        let countDown = 5;
+        
+        Swal.fire({
+            title: '密码修改成功',
+            html: `您的密码已成功修改,系统将在 <b>${countDown}</b> 秒后自动退出,请使用新密码重新登录。`,
+            icon: 'success',
+            timer: countDown * 1000,
+            timerProgressBar: true,
+            didOpen: () => {
+                const content = Swal.getHtmlContainer();
+                const timerInterval = setInterval(() => {
+                    countDown--;
+                    if (content) {
+                        const b = content.querySelector('b');
+                        if (b) {
+                            b.textContent = countDown > 0 ? countDown : 0;
+                        }
+                    }
+                    if (countDown <= 0) clearInterval(timerInterval);
+                }, 1000);
+            },
+            allowOutsideClick: false, // 禁止外部点击关闭
+            showConfirmButton: true, // 重新显示确认按钮
+            confirmButtonText: '确定' // 设置按钮文本
+        }).then((result) => {
+            // 当计时器结束或弹窗被关闭时 (包括点击确定按钮)
+            if (result.dismiss === Swal.DismissReason.timer || result.isConfirmed) {
+                console.log('计时器结束或手动确认,执行登出');
+                auth.logout();
+            }
+        });
+    } catch (error) {
+        console.error('修改密码失败:', error);
+        core.showAlert('修改密码失败: ' + error.message, 'error');
+    }
+}
+
+// 验证密码复杂度
+function isPasswordComplex(password) {
+    // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间
+    const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
+    return passwordRegex.test(password);
+}
+
+// 检查密码强度
+function checkUcPasswordStrength() {
+    const password = document.getElementById('ucNewPassword').value;
+    const strengthSpan = document.getElementById('ucPasswordStrength');
+    
+    if (!password) {
+        strengthSpan.textContent = '';
+        return;
+    }
+    
+    let strength = 0;
+    let strengthText = '';
+    
+    // 长度检查
+    if (password.length >= 8) strength++;
+    if (password.length >= 12) strength++;
+    
+    // 包含字母
+    if (/[A-Za-z]/.test(password)) strength++;
+    
+    // 包含数字
+    if (/\d/.test(password)) strength++;
+    
+    // 包含特殊字符
+    if (/[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]/.test(password)) strength++;
+    
+    // 根据强度设置文本和颜色
+    switch(strength) {
+        case 0:
+        case 1:
+            strengthText = '密码强度:非常弱';
+            strengthSpan.style.color = '#FF4136';
+            break;
+        case 2:
+            strengthText = '密码强度:弱';
+            strengthSpan.style.color = '#FF851B';
+            break;
+        case 3:
+            strengthText = '密码强度:中';
+            strengthSpan.style.color = '#FFDC00';
+            break;
+        case 4:
+            strengthText = '密码强度:强';
+            strengthSpan.style.color = '#2ECC40';
+            break;
+        case 5:
+            strengthText = '密码强度:非常强';
+            strengthSpan.style.color = '#3D9970';
+            break;
+    }
+    
+    strengthSpan.textContent = strengthText;
+}
+
+// 初始化用户中心
+function initUserCenter() {
+    console.log('初始化用户中心');
+    
+    // 获取用户信息
+    getUserInfo();
+    
+    // 为用户中心按钮添加事件
+    const userCenterBtn = document.getElementById('userCenterBtn');
+    if (userCenterBtn) {
+        userCenterBtn.addEventListener('click', () => {
+            core.showSection('user-center');
+        });
+    }
+}
+
+// 加载用户统计信息
+function loadUserStats() {
+    getUserInfo();
+}
+
+// 导出模块
+const userCenter = {
+    init: function() {
+        console.log('初始化用户中心模块...');
+        // 可以在这里调用初始化逻辑,也可以延迟到需要时调用
+        return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致
+    },
+    getUserInfo,
+    changePassword,
+    checkUcPasswordStrength,
+    initUserCenter,
+    loadUserStats,
+    isPasswordComplex
+};
+
+// 页面加载完成后初始化
+document.addEventListener('DOMContentLoaded', initUserCenter);
+
+// 全局公开用户中心模块
+window.userCenter = userCenter;

+ 224 - 0
hubcmdui/web/services/documentationService.js

@@ -0,0 +1,224 @@
+const fs = require('fs');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+// 文档存储目录
+const DOCUMENTATION_DIR = path.join(__dirname, '..', 'data', 'documentation');
+
+/**
+ * 确保文档目录存在
+ */
+async function ensureDocumentationDirExists() {
+    if (!fs.existsSync(DOCUMENTATION_DIR)) {
+        await fs.promises.mkdir(DOCUMENTATION_DIR, { recursive: true });
+        console.log(`创建文档目录: ${DOCUMENTATION_DIR}`);
+    }
+}
+
+/**
+ * 获取文档列表
+ * @returns {Promise<Array>} 文档列表
+ */
+async function getDocumentList() {
+    try {
+        await ensureDocumentationDirExists();
+        
+        // 检查索引文件是否存在
+        const indexPath = path.join(DOCUMENTATION_DIR, 'index.json');
+        if (!fs.existsSync(indexPath)) {
+            // 创建空索引,不再添加默认文档
+            await fs.promises.writeFile(indexPath, JSON.stringify([]), 'utf8');
+            console.log('创建了空的文档索引文件');
+            return [];
+        }
+        
+        // 读取索引文件
+        const data = await fs.promises.readFile(indexPath, 'utf8');
+        return JSON.parse(data || '[]');
+    } catch (error) {
+        console.error('获取文档列表失败:', error);
+        return [];
+    }
+}
+
+/**
+ * 保存文档列表
+ * @param {Array} docList 文档列表
+ */
+async function saveDocumentList(docList) {
+    try {
+        await ensureDocumentationDirExists();
+        
+        const indexPath = path.join(DOCUMENTATION_DIR, 'index.json');
+        await fs.promises.writeFile(indexPath, JSON.stringify(docList, null, 2), 'utf8');
+        console.log('文档列表已更新');
+    } catch (error) {
+        console.error('保存文档列表失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * 获取单个文档的内容
+ * @param {string} docId 文档ID
+ * @returns {Promise<string>} 文档内容
+ */
+async function getDocumentContent(docId) {
+    try {
+        console.log(`尝试获取文档内容,ID: ${docId}`);
+        
+        // 确保文档目录存在
+        await ensureDocumentationDirExists();
+        
+        // 获取文档列表
+        const docList = await getDocumentList();
+        const doc = docList.find(doc => doc.id === docId || doc._id === docId);
+        
+        if (!doc) {
+            throw new Error(`文档不存在,ID: ${docId}`);
+        }
+        
+        // 构建文档路径
+        const docPath = path.join(DOCUMENTATION_DIR, `${docId}.md`);
+        
+        // 检查文件是否存在
+        if (!fs.existsSync(docPath)) {
+            return ''; // 文件不存在,返回空内容
+        }
+        
+        // 读取文档内容
+        const content = await fs.promises.readFile(docPath, 'utf8');
+        console.log(`成功读取文档内容,ID: ${docId}, 内容长度: ${content.length}`);
+        
+        return content;
+    } catch (error) {
+        console.error(`获取文档内容失败,ID: ${docId}`, error);
+        throw error;
+    }
+}
+
+/**
+ * 创建或更新文档
+ * @param {Object} doc 文档对象
+ * @param {string} content 文档内容
+ * @returns {Promise<Object>} 保存后的文档
+ */
+async function saveDocument(doc, content) {
+    try {
+        await ensureDocumentationDirExists();
+        
+        // 获取现有文档列表
+        const docList = await getDocumentList();
+        
+        // 为新文档生成ID
+        if (!doc.id) {
+            doc.id = uuidv4();
+        }
+        
+        // 更新文档元数据
+        doc.lastUpdated = new Date().toISOString();
+        
+        // 查找现有文档索引
+        const existingIndex = docList.findIndex(item => item.id === doc.id);
+        
+        if (existingIndex >= 0) {
+            // 更新现有文档
+            docList[existingIndex] = { ...docList[existingIndex], ...doc };
+        } else {
+            // 添加新文档
+            docList.push(doc);
+        }
+        
+        // 保存文档内容
+        const docPath = path.join(DOCUMENTATION_DIR, `${doc.id}.md`);
+        await fs.promises.writeFile(docPath, content || '', 'utf8');
+        
+        // 更新文档列表
+        await saveDocumentList(docList);
+        
+        return doc;
+    } catch (error) {
+        console.error('保存文档失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * 删除文档
+ * @param {string} docId 文档ID
+ * @returns {Promise<boolean>} 删除结果
+ */
+async function deleteDocument(docId) {
+    try {
+        await ensureDocumentationDirExists();
+        
+        // 获取现有文档列表
+        const docList = await getDocumentList();
+        
+        // 查找文档索引
+        const existingIndex = docList.findIndex(doc => doc.id === docId);
+        
+        if (existingIndex === -1) {
+            return false; // 文档不存在
+        }
+        
+        // 从列表中移除
+        docList.splice(existingIndex, 1);
+        
+        // 删除文档文件
+        const docPath = path.join(DOCUMENTATION_DIR, `${docId}.md`);
+        if (fs.existsSync(docPath)) {
+            await fs.promises.unlink(docPath);
+        }
+        
+        // 更新文档列表
+        await saveDocumentList(docList);
+        
+        return true;
+    } catch (error) {
+        console.error('删除文档失败:', error);
+        throw error;
+    }
+}
+
+/**
+ * 发布或取消发布文档
+ * @param {string} docId 文档ID
+ * @param {boolean} publishState 发布状态
+ * @returns {Promise<Object>} 更新后的文档
+ */
+async function togglePublishState(docId, publishState) {
+    try {
+        // 获取现有文档列表
+        const docList = await getDocumentList();
+        
+        // 查找文档索引
+        const existingIndex = docList.findIndex(doc => doc.id === docId);
+        
+        if (existingIndex === -1) {
+            throw new Error('文档不存在');
+        }
+        
+        // 更新发布状态
+        docList[existingIndex].published = !!publishState;
+        docList[existingIndex].lastUpdated = new Date().toISOString();
+        
+        // 更新文档列表
+        await saveDocumentList(docList);
+        
+        return docList[existingIndex];
+    } catch (error) {
+        console.error('更新文档发布状态失败:', error);
+        throw error;
+    }
+}
+
+module.exports = {
+    ensureDocumentationDirExists,
+    getDocumentList,
+    saveDocumentList,
+    getDocumentContent,
+    saveDocument,
+    deleteDocument,
+    togglePublishState
+}; 

+ 782 - 13
hubcmdui/web/style.css

@@ -4,7 +4,7 @@
     --primary-color: #3D7CF4;
     --primary-light: #5D95FD;
     --primary-dark: #2F62C9;
-    
+    --primary-dark-color: #2c5282; /* 深蓝色,可以根据您的主题调整 */
     /* 辅助色 */
     --secondary-color: #FF6B6B;
     --secondary-light: #FF8E8E;
@@ -749,34 +749,169 @@
 }
 
 #documentationText {
-    padding: 0 1rem;
+    padding: 0.5rem 1.5rem; /* 增加左右内边距 */
+    max-width: 900px; /* 限制最大宽度以提高可读性 */
+    /* margin-left: auto;  移除左边距自动 */
+    /* margin-right: auto; 移除右边距自动 */
 }
 
 #documentationText h2 {
-    font-size: 1.8rem;
-    margin: 0 0 1.5rem 0;
-    color: var(--text-primary);
+    font-size: 1.8em;
+    margin-top: 2.5em;
+    margin-bottom: 1em;
+    border-bottom: 1px solid #eaecef;
+    padding-bottom: 0.4em;
+    font-weight: 600;
 }
 
 #documentationText p {
-    line-height: 1.7;
-    color: var(--text-secondary);
-    margin-bottom: 1.2rem;
+    margin-bottom: 1.5rem; /* 增大段落间距 */
+    font-size: 1.05rem; /* 稍微增大正文字号 */
 }
 
 #documentationText ul, #documentationText ol {
-    color: var(--text-secondary);
-    line-height: 1.7;
-    padding-left: 1.5rem;
+    padding-left: 1.8em; /* 调整缩进 */
     margin-bottom: 1.5rem;
 }
 
 #documentationText li {
-    margin-bottom: 0.5rem;
+    margin-bottom: 0.6rem;
 }
 
 #documentationText pre {
-    margin: 1.5rem 0;
+    background-color: #1F2937; /* 深色背景 */
+    color: #F3F4F6; /* 浅色文字 */
+    padding: 1.2rem 1.5rem; /* 调整内边距 */
+    border-radius: var(--radius-md);
+    overflow-x: auto;
+    margin: 1.8rem 0; /* 增加垂直外边距 */
+    line-height: 1.6; /* 调整行高 */
+    border: 1px solid #374151; /* 深色边框 */
+    font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; /* 使用适合编程的字体 */
+    font-size: 0.95rem; /* 标准化字体大小 */
+    position: relative; /* 为复制按钮定位 */
+}
+
+.doc-content pre::before {
+    content: ''; /* 模拟终端窗口的顶部栏 */
+    display: block;
+    height: 28px; /* 顶部栏高度 */
+    background-color: #111827; /* 顶部栏颜色 */
+    border-top-left-radius: var(--radius-md);
+    border-top-right-radius: var(--radius-md);
+    margin: -1.2rem -1.5rem 1rem -1.5rem; /* 定位和添加下方间距 */
+    position: relative;
+}
+
+/* 模拟窗口按钮 */
+.doc-content pre::after {
+    content: '';
+    position: absolute;
+    top: 8px;
+    left: 15px;
+    width: 12px;
+    height: 12px;
+    background-color: #FF5F57;
+    border-radius: 50%;
+    box-shadow: 20px 0 #FEBC2E, 40px 0 #28C840;
+}
+
+.doc-content pre code {
+    display: block; /* 确保代码块充满 pre */
+    background-color: transparent;
+    padding: 0;
+    margin: 0;
+    color: inherit;
+    border-radius: 0;
+    border: none;
+    line-height: inherit;
+    font-family: inherit;
+    white-space: pre; /* 保留空格和换行 */
+    font-size: inherit; /* 继承 pre 的字号 */
+}
+
+/* 行内代码样式 */
+.doc-content code {
+    font-family: 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+    font-size: 0.9em; /* 调整大小 */
+    background-color: rgba(61, 124, 244, 0.1); /* 更柔和的背景 */
+    padding: 0.25em 0.5em;
+    margin: 0 0.1em;
+    border-radius: 4px;
+    color: #2c5282; /* 主色调的深色 */
+    border: 1px solid rgba(61, 124, 244, 0.2);
+    vertical-align: middle; /* 垂直对齐 */
+}
+
+/* 链接样式 */
+.doc-content a {
+    color: var(--primary-dark); /* 使用更深的蓝色 */
+    text-decoration: underline;
+    text-decoration-color: rgba(61, 124, 244, 0.4);
+    transition: all 0.2s ease;
+}
+
+.doc-content a:hover {
+    color: var(--primary-color);
+    text-decoration-color: var(--primary-color);
+    background-color: rgba(61, 124, 244, 0.05);
+}
+
+/* 引用块样式 */
+.doc-content blockquote {
+    margin: 2em 0;
+    padding: 1em 1.5em;
+    color: #555;
+    border-left: 4px solid var(--primary-light);
+    background-color: #f8faff; /* 淡蓝色背景 */
+    border-radius: var(--radius-sm);
+}
+
+.doc-content blockquote p:last-child {
+    margin-bottom: 0;
+}
+
+/* 表格样式 */
+.doc-content table {
+    border-collapse: separate; /* 使用 separate 以应用圆角 */
+    border-spacing: 0;
+    margin: 1.8rem 0;
+    width: 100%;
+    border: 1px solid #e2e8f0;
+    border-radius: var(--radius-md);
+    overflow: hidden; /* 应用圆角 */
+}
+
+.doc-content th,
+.doc-content td {
+    border-bottom: 1px solid #e2e8f0;
+    padding: 0.8em 1.2em;
+    text-align: left;
+}
+
+.doc-content tr:last-child td {
+    border-bottom: none;
+}
+
+.doc-content th {
+    font-weight: 600;
+    background-color: #f7f9fc; /* 更浅的表头背景 */
+    color: #4a5568;
+}
+
+.doc-content tr:nth-child(even) td {
+    background-color: #fafcff; /* 斑马纹 */
+}
+
+/* 元数据 (更新时间) 样式 */
+.doc-meta {
+    font-size: 0.9em;
+    color: #888;
+    text-align: right; /* 右对齐 */
+    margin-top: 0rem; /* 调整与内容的距离 */
+    padding-top: 0.5rem; /* 增加顶部内边距 */
+    border-top: 1px dashed #eee; /* 放到顶部 */
+    clear: both; /* 确保在内容下方 */
 }
 
 /* 加载中和消息提示 */
@@ -2006,3 +2141,637 @@
     font-size: 1.1rem;
     text-align: center;
 }
+
+/* 登录页面特定样式 - 确保按钮正确显示 */
+.login-container .captcha-area {
+  display: flex !important;
+  align-items: center !important;
+  justify-content: space-between !important;
+  gap: 10px !important;
+  width: 100% !important;
+}
+
+.login-container #captcha {
+  flex: 1.5 !important;
+  min-width: 0 !important;
+  margin: 0 !important;
+}
+
+.login-container #captchaText {
+  flex: 1 !important;
+  min-width: 80px !important;
+  height: 44px !important;
+  text-align: center !important;
+  line-height: 44px !important;
+  background-color: #f5f7fa !important;
+  border: 1px solid #ddd !important;
+  border-radius: 5px !important;
+  margin: 0 !important;
+}
+
+.login-container .btn-login {
+  flex: 1 !important;
+  min-width: 80px !important;
+  height: 44px !important;
+  margin: 0 !important;
+  white-space: nowrap !important;
+  width: auto !important;
+  background-color: var(--primary-color) !important;
+  color: white !important;
+  display: flex !important;
+  align-items: center !important;
+  justify-content: center !important;
+}
+
+/* 处理登录按钮在hover时的行为 */
+.login-container .btn-login:hover {
+  background-color: var(--primary-dark) !important;
+  transform: none !important; /* 防止按钮hover时的上移效果导致布局变化 */
+}
+
+/* 确保登录表单中的其他按钮不受影响 */
+.login-container button:not(.btn-login) {
+  width: 100%;
+}
+
+/* 修复在移动设备上的显示 */
+@media (max-width: 480px) {
+  .login-container .captcha-area {
+    flex-wrap: wrap !important;
+  }
+  
+  .login-container #captcha {
+    flex: 100% !important;
+    margin-bottom: 10px !important;
+  }
+  
+  .login-container #captchaText {
+    flex: 1 !important;
+  }
+  
+  .login-container .btn-login {
+    flex: 1 !important;
+  }
+}
+
+/* 登录按钮样式 */
+#loginButton {
+  display: block !important;
+  width: 100% !important;
+  padding: 10px !important;
+  background-color: var(--primary-color) !important;
+  color: white !important;
+  border: none !important;
+  border-radius: 5px !important;
+  cursor: pointer !important;
+  font-size: 16px !important;
+  margin-top: 15px !important;
+}
+
+#loginButton:hover {
+  background-color: var(--primary-dark-color) !important;
+}
+
+.login-form button {
+  background-color: var(--primary-color) !important;
+  color: white !important;
+}
+
+.login-form button:hover {
+  background-color: var(--primary-dark-color) !important;
+  color: white !important;
+}
+
+.login-modal .login-content .login-form #loginButton:hover {
+  background-color: var(--primary-dark-color) !important;
+  color: white !important;
+}
+
+/* ======================
+   文档内容样式 (Doc Content) - 优化版
+   ====================== */
+
+/* 文档文本容器整体调整 */
+#documentationText {
+    padding: 0.5rem 1.5rem; /* 增加左右内边距 */
+    max-width: 900px; /* 限制最大宽度以提高可读性 */
+    /* margin-left: auto;  移除左边距自动 */
+    /* margin-right: auto; 移除右边距自动 */
+}
+
+.doc-content {
+    font-family: 'Georgia', 'Times New Roman', 'Source Han Serif CN', 'Songti SC', serif; /* 使用更适合阅读的衬线字体 */
+    line-height: 1.8; /* 增加行高 */
+    color: #333;
+    padding-top: 0; /* 移除顶部padding,让标题控制间距 */
+    margin-bottom: 2rem; /* 内容和元数据之间的间距 */
+}
+
+/* 标题样式调整 */
+.doc-content h1:first-of-type, /* 优先将第一个h1作为主标题 */
+.doc-content h2:first-of-type, 
+.doc-content h3:first-of-type { 
+    margin-top: 0; /* 移除主标题上方的间距 */
+    margin-bottom: 1rem; /* 主标题下方间距 */
+    padding-bottom: 0.5rem; /* 标题下划线间距 */
+    border-bottom: 2px solid var(--primary-color); /* 强调主标题下划线 */
+    font-size: 2.5em; /* 增大主标题字号 */
+    font-weight: 700;
+    line-height: 1.2;
+}
+
+.doc-content h2 {
+    font-size: 1.8em;
+    margin-top: 2.5em;
+    margin-bottom: 1em;
+    border-bottom: 1px solid #eaecef;
+    padding-bottom: 0.4em;
+    font-weight: 600;
+}
+
+.doc-content h3 {
+    font-size: 1.5em;
+    margin-top: 2em;
+    margin-bottom: 0.8em;
+    font-weight: 600;
+}
+
+.doc-content h4 {
+    font-size: 1.25em;
+    margin-top: 1.8em;
+    margin-bottom: 0.7em;
+    font-weight: 600;
+    color: #444;
+}
+
+/* 段落样式 */
+.doc-content p {
+    margin-bottom: 1.5rem; /* 增大段落间距 */
+    font-size: 1.05rem; /* 稍微增大正文字号 */
+}
+
+/* 列表样式 */
+.doc-content ul,
+.doc-content ol {
+    padding-left: 1.8em; /* 调整缩进 */
+    margin-bottom: 1.5rem;
+}
+
+.doc-content li {
+    margin-bottom: 0.6rem;
+}
+
+/* 元数据 (更新时间) 样式 */
+.doc-meta {
+    font-size: 0.9em;
+    color: #888;
+    text-align: right; /* 右对齐 */
+    margin-top: 0rem; /* 调整与内容的距离 */
+    padding-top: 0.5rem; /* 增加顶部内边距 */
+    border-top: 1px dashed #eee; /* 放到顶部 */
+    clear: both; /* 确保在内容下方 */
+}
+
+/* 代码块样式统一 */
+.doc-content pre {
+    background-color: #1F2937; /* 深色背景 */
+    color: #F3F4F6; /* 浅色文字 */
+    padding: 1.2rem 1.5rem; /* 调整内边距 */
+    border-radius: var(--radius-md);
+    overflow-x: auto;
+    margin: 1.8rem 0; /* 增加垂直外边距 */
+    line-height: 1.6; /* 调整行高 */
+    border: 1px solid #374151; /* 深色边框 */
+    font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; /* 使用适合编程的字体 */
+    font-size: 0.95rem; /* 标准化字体大小 */
+    position: relative; /* 为复制按钮定位 */
+}
+
+.doc-content pre::before {
+    content: ''; /* 模拟终端窗口的顶部栏 */
+    display: block;
+    height: 28px; /* 顶部栏高度 */
+    background-color: #111827; /* 顶部栏颜色 */
+    border-top-left-radius: var(--radius-md);
+    border-top-right-radius: var(--radius-md);
+    margin: -1.2rem -1.5rem 1rem -1.5rem; /* 定位和添加下方间距 */
+    position: relative;
+}
+
+/* 模拟窗口按钮 */
+.doc-content pre::after {
+    content: '';
+    position: absolute;
+    top: 8px;
+    left: 15px;
+    width: 12px;
+    height: 12px;
+    background-color: #FF5F57;
+    border-radius: 50%;
+    box-shadow: 20px 0 #FEBC2E, 40px 0 #28C840;
+}
+
+.doc-content pre code {
+    display: block; /* 确保代码块充满 pre */
+    background-color: transparent;
+    padding: 0;
+    margin: 0;
+    color: inherit;
+    border-radius: 0;
+    border: none;
+    line-height: inherit;
+    font-family: inherit;
+    white-space: pre; /* 保留空格和换行 */
+    font-size: inherit; /* 继承 pre 的字号 */
+}
+
+/* 行内代码样式 */
+.doc-content code {
+    font-family: 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+    font-size: 0.9em; /* 调整大小 */
+    background-color: rgba(61, 124, 244, 0.1); /* 更柔和的背景 */
+    padding: 0.25em 0.5em;
+    margin: 0 0.1em;
+    border-radius: 4px;
+    color: #2c5282; /* 主色调的深色 */
+    border: 1px solid rgba(61, 124, 244, 0.2);
+    vertical-align: middle; /* 垂直对齐 */
+}
+
+/* 链接样式 */
+.doc-content a {
+    color: var(--primary-dark); /* 使用更深的蓝色 */
+    text-decoration: underline;
+    text-decoration-color: rgba(61, 124, 244, 0.4);
+    transition: all 0.2s ease;
+}
+
+.doc-content a:hover {
+    color: var(--primary-color);
+    text-decoration-color: var(--primary-color);
+    background-color: rgba(61, 124, 244, 0.05);
+}
+
+/* 引用块样式 */
+.doc-content blockquote {
+    margin: 2em 0;
+    padding: 1em 1.5em;
+    color: #555;
+    border-left: 4px solid var(--primary-light);
+    background-color: #f8faff; /* 淡蓝色背景 */
+    border-radius: var(--radius-sm);
+}
+
+.doc-content blockquote p:last-child {
+    margin-bottom: 0;
+}
+
+/* 表格样式 */
+.doc-content table {
+    border-collapse: separate; /* 使用 separate 以应用圆角 */
+    border-spacing: 0;
+    margin: 1.8rem 0;
+    width: 100%;
+    border: 1px solid #e2e8f0;
+    border-radius: var(--radius-md);
+    overflow: hidden; /* 应用圆角 */
+}
+
+.doc-content th,
+.doc-content td {
+    border-bottom: 1px solid #e2e8f0;
+    padding: 0.8em 1.2em;
+    text-align: left;
+}
+
+.doc-content tr:last-child td {
+    border-bottom: none;
+}
+
+.doc-content th {
+    font-weight: 600;
+    background-color: #f7f9fc; /* 更浅的表头背景 */
+    color: #4a5568;
+}
+
+.doc-content tr:nth-child(even) td {
+    background-color: #fafcff; /* 斑马纹 */
+}
+
+/* 提示消息增强样式 - 修改为居中模态样式 */
+.toast-notification {
+  position: fixed;
+  /* top: 20px; */ /* 移除顶部定位 */
+  top: 50%; /* 垂直居中 */
+  left: 50%;
+  transform: translate(-50%, -50%); /* 水平垂直居中 */
+  background-color: white; /* 使用白色背景更像弹窗 */
+  color: var(--text-primary);
+  padding: 2rem 2.5rem; /* 增加内边距 */
+  border-radius: var(--radius-lg); /* 更大的圆角 */
+  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); /* 更明显的阴影 */
+  z-index: 1100;
+  /* animation: fadeIn 0.3s ease-in; */ /* 可以保留或换成缩放动画 */
+  animation: fadeInScale 0.3s ease-out; /* 使用新的缩放动画 */
+  text-align: center;
+  min-width: 300px; /* 设置最小宽度 */
+  max-width: 90%;
+  display: flex;
+  flex-direction: column; /* 让图标和文字垂直排列 */
+  align-items: center;
+  gap: 1rem; /* 图标和文字之间的间距 */
+  border-left: none; /* 移除之前的左边框 */
+  border-top: 5px solid var(--info-color); /* 使用顶部边框指示类型 */
+}
+
+/* 不同类型的顶部边框颜色 */
+.toast-notification.error {
+  border-top-color: var(--danger-color);
+}
+
+.toast-notification.success {
+    border-top-color: var(--success-color);
+}
+
+.toast-notification.info {
+  border-top-color: var(--info-color);
+}
+
+/* 图标样式 */
+.toast-notification i {
+  font-size: 2.5rem; /* 增大图标 */
+  margin-bottom: 0.5rem; /* 图标下方间距 */
+}
+
+.toast-notification.error i {
+  color: var(--danger-color);
+}
+
+.toast-notification.success i {
+    color: var(--success-color);
+}
+
+.toast-notification.info i {
+  color: var(--info-color);
+}
+
+/* 消息文本样式 */
+.toast-notification span {
+    font-size: 1.1rem;
+    line-height: 1.5;
+}
+
+/* 淡出动画 */
+.toast-notification.fade-out {
+  opacity: 0;
+  transform: translate(-50%, -50%) scale(0.9); /* 淡出时缩小 */
+  transition: opacity 0.3s ease-out, transform 0.3s ease-out;
+}
+
+/* 新增:淡入和缩放动画 */
+@keyframes fadeInScale {
+  from {
+    opacity: 0;
+    transform: translate(-50%, -50%) scale(0.8);
+  }
+  to {
+    opacity: 1;
+    transform: translate(-50%, -50%) scale(1);
+  }
+}
+
+/* ... 其他样式 ... */
+
+/* 表格通用样式 */
+.admin-container table {
+    /* ... (现有表格样式) ... */
+}
+
+/* Docker 状态表格特定样式 */
+#dockerStatusTable {
+    table-layout: fixed; /* 固定表格布局,让列宽设定生效 */
+    width: 100%;
+    border-collapse: collapse; /* 合并边框 */
+    margin-top: 1rem; /* 与上方元素保持距离 */
+}
+
+#dockerStatusTable th,
+#dockerStatusTable td {
+    padding: 0.8rem 1rem; /* 统一内边距 */
+    border: 1px solid var(--border-light, #e5e7eb); /* 添加边框 */
+    vertical-align: middle; 
+    font-size: 0.9rem; /* 稍小字体 */
+}
+
+#dockerStatusTable thead th {
+    background-color: var(--table-header-bg, #f9fafb); /* 表头背景色 */
+    color: var(--text-secondary, #6b7280);
+    font-weight: 600; /* 表头字体加粗 */
+    position: sticky; /* 尝试粘性定位,如果表格滚动 */
+    top: 0; /* 配合 sticky */
+    z-index: 1; /* 确保表头在加载提示之上 */
+}
+
+/* 为 ID 列设置较窄宽度 */
+#dockerStatusTable th:nth-child(1),
+#dockerStatusTable td:nth-child(1) {
+    width: 120px; /* 或 10% */
+}
+
+/* 为 名称 列设置最大宽度和文本溢出处理 */
+#dockerStatusTable th:nth-child(2),
+#dockerStatusTable td:nth-child(2) {
+    max-width: 200px; /* 根据需要调整 */
+    width: 25%; /* 尝试百分比宽度 */
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+/* 为 镜像 列设置最大宽度和文本溢出处理 */
+#dockerStatusTable th:nth-child(3),
+#dockerStatusTable td:nth-child(3) {
+    max-width: 300px; /* 可以比名称宽一些 */
+    width: 35%; /* 尝试百分比宽度 */
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}
+
+/* 为 状态 列设置宽度 */
+#dockerStatusTable th:nth-child(4),
+#dockerStatusTable td:nth-child(4) {
+     width: 100px; /* 或 10% */
+     text-align: center; /* 状态居中 */
+}
+
+/* 为 操作 列设置宽度 */
+#dockerStatusTable th:nth-child(5),
+#dockerStatusTable td:nth-child(5) {
+    width: 150px; /* 或 20% */
+    text-align: center; /* 操作按钮居中 */
+}
+
+/* 确保 title 属性的提示能正常显示 */
+#dockerStatusTable td:nth-child(2),
+#dockerStatusTable td:nth-child(3) {
+    cursor: default; /* 或 help,提示用户悬停可查看完整信息 */
+}
+
+/* 加载提示行样式调整 */
+#dockerStatusTable .loading-container td {
+    background-color: rgba(255, 255, 255, 0.8); /* 半透明背景,避免完全遮挡 */
+    padding: 2rem 0; /* 增加垂直内边距 */
+    text-align: center;
+    color: var(--text-secondary);
+}
+
+/* --- 新增:操作下拉菜单样式 --- */
+.action-cell {
+    position: relative; /* 确保下拉菜单相对于单元格定位 */
+    text-align: center; /* 让按钮居中 */
+}
+
+.action-dropdown .dropdown-toggle {
+    padding: 0.3rem 0.6rem;
+    font-size: 0.85rem; /* 按钮字体小一点 */
+    background-color: var(--container-bg);
+    border: 1px solid var(--border-color);
+    color: var(--text-secondary);
+}
+
+.action-dropdown .dropdown-toggle:hover,
+.action-dropdown .dropdown-toggle:focus {
+    background-color: var(--background-color);
+    border-color: var(--border-dark);
+    color: var(--text-primary);
+}
+
+.action-dropdown .dropdown-menu {
+    min-width: 160px; /* 设置最小宽度 */
+    box-shadow: var(--shadow-md);
+    border: 1px solid var(--border-light);
+    padding: 0.5rem 0;
+    margin-top: 0.25rem; /* 微调与按钮的距离 */
+}
+
+.action-dropdown .dropdown-item {
+    display: flex;
+    align-items: center;
+    gap: 0.75rem;
+    padding: 0.6rem 1.2rem; /* 调整内边距 */
+    font-size: 0.9rem;
+    color: var(--text-secondary);
+    cursor: pointer;
+    white-space: nowrap;
+}
+
+.action-dropdown .dropdown-item:hover {
+    background-color: var(--background-color);
+    color: var(--text-primary);
+}
+
+.action-dropdown .dropdown-item i {
+    width: 16px; /* 固定图标宽度 */
+    text-align: center;
+    color: var(--text-muted);
+    transition: color var(--transition-fast);
+}
+
+.action-dropdown .dropdown-item:hover i {
+     color: var(--primary-color);
+}
+
+.action-dropdown .dropdown-item.text-danger,
+.action-dropdown .dropdown-item.text-danger:hover {
+    color: var(--danger-color) !important;
+}
+.action-dropdown .dropdown-item.text-danger i {
+     color: var(--danger-color) !important;
+}
+/* --- 结束:操作下拉菜单样式 --- */
+
+
+/* --- 新增:详情弹窗表格样式 --- */
+.details-swal-popup .details-table-container {
+    max-height: 60vh; /* 限制最大高度,出现滚动条 */
+    overflow-y: auto;
+    margin-top: 1rem;
+}
+
+.details-swal-popup .details-table {
+    width: 100%;
+    border-collapse: collapse;
+    font-size: 0.9rem;
+}
+
+.details-swal-popup .details-table thead th {
+    background-color: var(--table-header-bg, #f9fafb);
+    color: var(--text-secondary, #6b7280);
+    font-weight: 600;
+    text-align: left; /* 确保表头左对齐 */
+    padding: 0.8rem 1rem;
+    border-bottom: 2px solid var(--border-dark, #e5e7eb);
+    position: sticky; /* 表头粘性定位 */
+    top: 0;
+    z-index: 1;
+}
+
+.details-swal-popup .details-table tbody td {
+    padding: 0.8rem 1rem;
+    border-bottom: 1px solid var(--border-light, #e5e7eb);
+    vertical-align: middle;
+    color: var(--text-primary);
+    text-align: left; /* 确保单元格左对齐 */
+}
+
+.details-swal-popup .details-table tbody tr:last-child td {
+    border-bottom: none;
+}
+
+.details-swal-popup .details-table tbody tr:hover td {
+    background-color: var(--background-color, #f8f9fa);
+}
+
+.details-swal-popup .details-table .badge {
+     font-size: 0.8rem; /* 状态徽章字体稍小 */
+}
+/* --- 结束:详情弹窗表格样式 --- */
+
+/* --- 新增:资源详情表格特殊样式 (两列) --- */
+.details-swal-popup .resource-details-table td.label {
+    font-weight: 600; /* 标签加粗 */
+    width: 40%; /* 设定标签列宽度 */
+    color: var(--text-secondary); /* 标签颜色稍浅 */
+    padding-right: 1.5rem; /* 标签和值之间的距离 */
+    white-space: nowrap; /* 防止标签换行 */
+}
+
+.details-swal-popup .resource-details-table td {
+    border-bottom: 1px dashed var(--border-light, #e5e7eb); /* 使用虚线分隔行 */
+}
+/* --- 结束:资源详情表格特殊样式 --- */
+
+/* --- 新增:资源详情类 Excel 表格样式 --- */
+.details-swal-popup .resource-details-excel {
+    table-layout: fixed; /* 固定布局 */
+    width: 100%;
+    margin-top: 0; /* 移除与容器的顶部距离 */
+}
+
+.details-swal-popup .resource-details-excel thead th {
+    text-align: center; /* 表头居中 */
+    white-space: nowrap;
+    border: 1px solid var(--border-dark, #dee2e6); /* 使用更深的边框 */
+    padding: 0.6rem 0.5rem; /* 调整内边距 */
+}
+
+.details-swal-popup .resource-details-excel tbody td {
+    text-align: center; /* 数据居中 */
+    padding: 0.6rem 0.5rem;
+    border: 1px solid var(--border-color, #dee2e6);
+    word-break: break-all; /* 允许长内容换行 */
+}
+/* --- 结束:资源详情类 Excel 表格样式 --- */
+
+/* ... 其他样式 ... */