Explorar o código

feat: 调整数据存储模式为SQLite,优化代码结构

dqzboy hai 3 meses
pai
achega
daadff788a
Modificáronse 45 ficheiros con 4455 adicións e 2214 borrados
  1. 1 1
      Dockerfile
  2. 1 1
      Issue/issue.md
  3. 21 0
      hubcmdui/.env.example
  4. 101 11
      hubcmdui/README.md
  5. 59 14
      hubcmdui/app.js
  6. 23 1
      hubcmdui/cleanup.js
  7. 22 29
      hubcmdui/compatibility-layer.js
  8. 0 1
      hubcmdui/config/menu.json
  9. 0 29
      hubcmdui/data/config.json
  10. 390 0
      hubcmdui/database/database.js
  11. 8 6
      hubcmdui/docker-compose.yaml
  12. BIN=BIN
      hubcmdui/documentation/.DS_Store
  13. 0 7
      hubcmdui/documentation/1743542841590.json
  14. 0 7
      hubcmdui/documentation/1743543376091.json
  15. 0 7
      hubcmdui/documentation/1743543400369.json
  16. 1 3
      hubcmdui/init-dirs.js
  17. 7 4
      hubcmdui/package.json
  18. 27 38
      hubcmdui/routes/auth.js
  19. 69 168
      hubcmdui/routes/config.js
  20. 43 460
      hubcmdui/routes/documentation.js
  21. 167 0
      hubcmdui/routes/httpProxy.js
  22. 5 5
      hubcmdui/routes/system.js
  23. 117 0
      hubcmdui/scripts/auto-setup.js
  24. 199 0
      hubcmdui/scripts/init-complete.js
  25. 92 0
      hubcmdui/scripts/init-database.js
  26. 0 315
      hubcmdui/scripts/init-system.js
  27. 45 7
      hubcmdui/server.js
  28. 0 47
      hubcmdui/services/configService.js
  29. 233 0
      hubcmdui/services/configServiceDB.js
  30. 0 324
      hubcmdui/services/documentationService.js
  31. 242 0
      hubcmdui/services/documentationServiceDB.js
  32. 418 0
      hubcmdui/services/httpProxyService.js
  33. 6 6
      hubcmdui/services/monitoringService.js
  34. 0 175
      hubcmdui/services/userService.js
  35. 190 0
      hubcmdui/services/userServiceDB.js
  36. 0 10
      hubcmdui/users.json
  37. 131 0
      hubcmdui/utils/database-checker.js
  38. 275 23
      hubcmdui/web/admin.html
  39. 84 0
      hubcmdui/web/css/admin.css
  40. 500 0
      hubcmdui/web/document-editor.html
  41. 178 21
      hubcmdui/web/index.html
  42. 5 1
      hubcmdui/web/js/core.js
  43. 5 401
      hubcmdui/web/js/documentManager.js
  44. 321 0
      hubcmdui/web/js/httpProxyManager.js
  45. 469 92
      hubcmdui/web/style.css

+ 1 - 1
Dockerfile

@@ -8,4 +8,4 @@ RUN npm install
 # 暴露应用程序的端口
 EXPOSE 3000
 # 运行应用程序
-CMD ["node", "server.js"]
+CMD ["npm", "start"]

+ 1 - 1
Issue/issue.md

@@ -150,7 +150,7 @@ proxy:
   ```
 
 
-#### 13、解决国内服务器上hubcmdui无法使用http代理请求
+#### 13、[项目已实现]解决国内服务器上hubcmdui无法使用http代理请求
 简单的讲,需要解决两个问题:
 1. dns污染,请自行搭建smartdns服务
 2. 修改axios.get相关代码

+ 21 - 0
hubcmdui/.env.example

@@ -0,0 +1,21 @@
+# HubCmdUI 环境变量配置示例
+# 复制此文件为 .env 并修改相应的值
+
+# HTTP 代理配置 (用于所有出站请求)
+# HTTP_PROXY=http://proxy.example.com:8080
+# HTTPS_PROXY=https://proxy.example.com:8080
+# NO_PROXY=localhost,127.0.0.1,.local
+
+# 如果代理需要用户名和密码认证
+# HTTP_PROXY=http://username:[email protected]:8080
+# HTTPS_PROXY=https://username:[email protected]:8080
+
+
+# 服务器配置
+PORT=3000
+
+# 会话密钥
+SESSION_SECRET=OhTq3faqSKoxbV%NJV
+
+# 服务器端口
+PORT=3000

+ 101 - 11
hubcmdui/README.md

@@ -26,22 +26,27 @@
 
 ---
 
-## 📝 源码构建运行
-#### 1. 克隆项目
-```bash
-git clone [email protected]:dqzboy/Docker-Proxy.git
-```
+## 📝 源码运行
 
-#### 2. 安装依赖
 ```bash
+# 克隆项目并启动
+git clone [email protected]:dqzboy/Docker-Proxy.git
 cd Docker-Proxy/hubcmdui
 npm install
+npm start
 ```
 
-#### 3. 启动服务
-```bash
-node server.js
-```
+系统会自动检测并完成:
+- ✅ 依赖包安装(如果需要)
+- ✅ SQLite数据库初始化(如果需要)
+- ✅ 启动服务
+
+
+### 访问系统
+
+- **主页**: http://localhost:3000
+- **管理面板**: http://localhost:3000/admin  
+- **默认账户**: root / admin@123
 
 ## 📦 Docker 方式运行
 
@@ -52,7 +57,7 @@ docker pull dqzboy/hubcmd-ui:latest
 
 #### 2. 运行 hubcmd-ui 容器
 ```bash
-docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data/config:/app/data -v ./data/docs:/app/documentation -v ./data/user/users.json:/app/users.json -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui
+docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data:/app/data -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui
 ```
 - `-v` 参数解释:左边是宿主机上的 Docker socket 文件路径,右边是容器内的映射路径
 
@@ -71,6 +76,56 @@ docker logs -f [容器ID或名称]
 
 ---
 
+## 🌐 代理配置
+
+支持通过环境变量配置 HTTP 代理,用于所有出站网络请求。
+
+### 环境变量配置
+
+```bash
+# HTTP 代理配置
+export HTTP_PROXY=http://proxy.example.com:8080
+export HTTPS_PROXY=https://proxy.example.com:8080
+export NO_PROXY=localhost,127.0.0.1,.local
+
+# 启动应用
+npm start
+```
+
+### Docker 部署代理配置
+
+```bash
+docker run -d \
+  -e HTTP_PROXY=http://proxy.example.com:8080 \
+  -e HTTPS_PROXY=https://proxy.example.com:8080 \
+  -e NO_PROXY=localhost,127.0.0.1,.local \
+  -v /var/run/docker.sock:/var/run/docker.sock \
+  -v ./data:/app/data \
+  -p 30080:3000 \
+  dqzboy/hubcmd-ui
+```
+
+### Docker Compose 代理配置
+
+```yaml
+version: '3.8'
+services:
+  hubcmdui:
+    image: dqzboy/hubcmd-ui
+    environment:
+      - HTTP_PROXY=http://proxy.example.com:8080
+      - HTTPS_PROXY=https://proxy.example.com:8080
+      - NO_PROXY=localhost,127.0.0.1,.local
+    volumes:
+      - /var/run/docker.sock:/var/run/docker.sock
+      # SQLite数据库文件
+      - ./data:/app/data
+    ports:
+      - "30080:3000"
+```
+
+---
+
 ## UI界面
 
 - 默认容器监听`3000`端口,映射宿主机端口`30080`
@@ -141,6 +196,41 @@ docker logs -f [容器ID或名称]
 
 ---
 
+## 🚀 系统特性
+
+### 数据存储优化
+- **SQLite数据库**: 所有数据统一存储在SQLite数据库中
+- **Session管理**: 使用数据库存储用户会话,自动清理过期会话
+- **配置管理**: 系统配置、用户数据、文档内容统一存储
+- **零文件依赖**: 不再依赖JSON文件存储,简化部署和维护
+
+### 功能特性
+- 🔐 **用户认证**: 基于数据库的用户管理系统
+- ⚙️ **配置管理**: 灵活的系统配置和菜单管理
+- 📚 **文档系统**: 内置Markdown文档管理
+- 🔍 **镜像搜索**: Docker Hub镜像搜索和代理
+- 📊 **系统监控**: 实时系统状态监控
+- 🎨 **响应式界面**: 现代化的Web管理界面
+
+## 📁 项目结构
+
+```
+hubcmdui/
+├── database/           # SQLite数据库相关
+│   └── database.js    # 数据库管理模块
+├── services/          # 业务服务层
+│   ├── configServiceDB.js    # 配置服务
+│   ├── userServiceDB.js      # 用户服务
+│   └── documentationServiceDB.js # 文档服务
+├── routes/            # API路由
+├── web/              # 前端静态文件
+├── middleware/       # 中间件
+└── data/             # 数据目录(SQLite文件)
+    └── app.db        # SQLite数据库文件
+```
+
+---
+
 ## 🫶 赞助
 如果你觉得这个项目对你有帮助,请给我点个Star。并且情况允许的话,可以给我一点点支持,总之非常感谢支持😊
 

+ 59 - 14
hubcmdui/app.js

@@ -20,8 +20,8 @@ const { requireLogin, sessionActivity, sanitizeRequestBody, securityHeaders } =
 console.log(`服务器启动,时间戳: ${global.serverStartTime}`);
 logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`);
 
-// 添加 session 文件存储模块 - 先导入session-file-store并创建对象
-const FileStore = require('session-file-store')(session);
+// 使用 SQLite 存储 session - 替代文件存储
+const SQLiteStore = require('connect-sqlite3')(session);
 
 // 确保目录结构存在
 ensureDirectoriesExist().catch(err => {
@@ -43,7 +43,7 @@ app.use(sessionActivity);
 app.use(sanitizeRequestBody);
 app.use(securityHeaders);
 
-// 会话配置
+// 会话配置 - 使用SQLite存储
 app.use(session({
   secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key',
   resave: false,
@@ -52,9 +52,10 @@ app.use(session({
     secure: process.env.NODE_ENV === 'production',
     maxAge: 24 * 60 * 60 * 1000 // 24小时
   },
-  store: new FileStore({
-    path: path.join(__dirname, 'data', 'sessions'),
-    ttl: 86400
+  store: new SQLiteStore({
+    db: 'app.db',
+    dir: path.join(__dirname, 'data'),
+    table: 'sessions'
   })
 }));
 
@@ -118,23 +119,67 @@ server.listen(PORT, async () => {
   try {
     // 确保目录存在
     await ensureDirectoriesExist();
+    
+    // 启动Session清理任务
+    await startSessionCleanupTask();
+    
     logger.success('系统初始化完成');
   } catch (error) {
     logger.error('系统初始化失败:', error);
   }
 });
 
-// 注册进程事件处理
-process.on('SIGINT', () => {
+// 启动定期清理过期会话的任务
+async function startSessionCleanupTask() {
+  const database = require('./database/database');
+  
+  // 立即清理一次
+  try {
+    await database.cleanExpiredSessions();
+  } catch (error) {
+    logger.error('清理过期会话失败:', error);
+  }
+  
+  // 每小时清理一次过期会话
+  setInterval(async () => {
+    try {
+      await database.cleanExpiredSessions();
+    } catch (error) {
+      logger.error('定期清理过期会话失败:', error);
+    }
+  }, 60 * 60 * 1000); // 1小时
+}
+
+// 监听进程退出事件,确保数据库连接正确关闭
+process.on('SIGINT', async () => {
+  logger.info('收到SIGINT信号,正在关闭服务器...');
+  const database = require('./database/database');
+  await database.close();
+  process.exit(0);
+});
+
+process.on('SIGTERM', async () => {
+  logger.info('收到SIGTERM信号,正在关闭服务器...');
+  const database = require('./database/database');
+  await database.close();
+  process.exit(0);
+});
+
+// 注册进程事件处理 - 优雅关闭
+process.on('SIGINT', async () => {
   logger.info('接收到中断信号,正在关闭服务...');
+  const database = require('./database/database');
+  await database.close();
   server.close(() => {
     logger.info('服务器已关闭');
     process.exit(0);
   });
 });
 
-process.on('SIGTERM', () => {
+process.on('SIGTERM', async () => {
   logger.info('接收到终止信号,正在关闭服务...');
+  const database = require('./database/database');
+  await database.close();
   server.close(() => {
     logger.info('服务器已关闭');
     process.exit(0);
@@ -169,14 +214,14 @@ function registerRoutes(app) {
     app.use('/api', authRouter);
     logger.info('认证路由已注册');
     
-    // 配置路由 - 函数式注册
+    // 配置路由 - 使用 Express Router
     const configRouter = require('./routes/config');
-    if (typeof configRouter === 'function') {
-      logger.info('配置路由是一个函数,正在注册...');
-      configRouter(app);
+    if (configRouter && typeof configRouter === 'object') {
+      logger.info('配置路由是一个 Router 对象,正在注册...');
+      app.use('/api/config', configRouter);
       logger.info('配置路由已注册');
     } else {
-      logger.error('配置路由不是一个函数,无法注册', typeof configRouter);
+      logger.error('配置路由不是一个有效的 Router 对象,无法注册', typeof configRouter);
     }
     
     logger.success('✓ 所有路由已注册');

+ 23 - 1
hubcmdui/cleanup.js

@@ -25,11 +25,33 @@ process.on('SIGINT', gracefulShutdown);
 process.on('SIGTERM', gracefulShutdown);
 
 // 优雅退出函数
-function gracefulShutdown() {
+async function gracefulShutdown() {
   logger.info('接收到退出信号,正在关闭...');
   
   // 这里可以添加清理代码,如关闭数据库连接等
   try {
+    // 关闭HTTP代理服务
+    try {
+      const httpProxyService = require('./services/httpProxyService');
+      if (httpProxyService && httpProxyService.isRunning) {
+        logger.info('正在关闭HTTP代理服务...');
+        await httpProxyService.stop();
+      }
+    } catch (err) {
+      logger.debug('HTTP代理服务未运行,跳过清理');
+    }
+
+    // 关闭数据库连接
+    try {
+      const database = require('./database/database');
+      if (database) {
+        logger.info('正在关闭数据库连接...');
+        await database.close();
+      }
+    } catch (err) {
+      logger.debug('数据库未连接,跳过清理');
+    }
+    
     // 关闭任何可能的资源
     try {
       const docker = require('./services/dockerService').getDockerConnection();

+ 22 - 29
hubcmdui/compatibility-layer.js

@@ -202,7 +202,7 @@ module.exports = function(app) {
   // 文档接口
   app.get('/api/documentation', async (req, res) => {
     try {
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       const documents = await docService.getPublishedDocuments();
       res.json(documents);
     } catch (error) {
@@ -324,7 +324,7 @@ module.exports = function(app) {
   // 获取单个文档接口
   app.get('/api/documentation/:id', async (req, res) => {
     try {
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       const document = await docService.getDocument(req.params.id);
       
       // 如果文档不是发布状态,只有已登录用户才能访问
@@ -345,7 +345,7 @@ module.exports = function(app) {
   // 文档列表接口
   app.get('/api/documentation-list', requireLogin, async (req, res) => {
     try {
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       const documents = await docService.getDocumentationList();
       res.json(documents);
     } catch (error) {
@@ -586,28 +586,21 @@ module.exports = function(app) {
         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);
+      const userServiceDB = require('./services/userServiceDB');
+      const user = await userServiceDB.validateUser(username, password);
       
       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: '用户名或密码错误' });
-      }
+      req.session.user = { username: user.username };
+      
+      // 更新用户登录信息
+      await userServiceDB.updateUserLoginInfo(username);
+      
+      logger.info(`User ${username} logged in successfully`);
+      res.json({ success: true });
     } catch (error) {
       logger.error('登录失败:', error);
       res.status(500).json({ error: '登录处理失败', details: error.message });
@@ -781,7 +774,7 @@ module.exports = function(app) {
   app.get('/api/documents', requireLogin, async (req, res) => {
     try {
       logger.info('兼容层处理获取文档列表请求');
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       const documents = await docService.getDocumentationList();
       res.json(documents);
     } catch (err) {
@@ -794,7 +787,7 @@ module.exports = function(app) {
   app.get('/api/documents/:id', async (req, res) => {
     try {
       logger.info(`兼容层处理获取文档请求: ${req.params.id}`);
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       const document = await docService.getDocument(req.params.id);
       
       // 如果文档不是发布状态,只有已登录用户才能访问
@@ -817,7 +810,7 @@ module.exports = function(app) {
     try {
       logger.info(`兼容层处理更新文档请求: ${req.params.id}`);
       const { title, content, published } = req.body;
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       
       // 检查必需参数
       if (!title) {
@@ -839,7 +832,7 @@ module.exports = function(app) {
     try {
       logger.info('兼容层处理创建文档请求');
       const { title, content, published } = req.body;
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       
       // 检查必需参数
       if (!title) {
@@ -861,7 +854,7 @@ module.exports = function(app) {
   app.delete('/api/documents/:id', requireLogin, async (req, res) => {
     try {
       logger.info(`兼容层处理删除文档请求: ${req.params.id}`);
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       
       await docService.deleteDocument(req.params.id);
       res.json({ success: true, message: '文档已删除' });
@@ -875,7 +868,7 @@ module.exports = function(app) {
   app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
     try {
       logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`);
-      const docService = require('./services/documentationService');
+      const docService = require('./services/documentationServiceDB');
       
       const result = await docService.toggleDocumentPublish(req.params.id);
       res.json({ 
@@ -924,8 +917,8 @@ module.exports = function(app) {
   // 用户信息接口
   app.get('/api/user-info', requireLogin, async (req, res) => {
     try {
-      const userService = require('./services/userService');
-      const userStats = await userService.getUserStats(req.session.user.username);
+      const userServiceDB = require('./services/userServiceDB');
+      const userStats = await userServiceDB.getUserStats(req.session.user.username);
       
       res.json(userStats);
     } catch (error) {
@@ -944,8 +937,8 @@ module.exports = function(app) {
       }
       
       try {
-          const userService = require('./services/userService');
-          await userService.changePassword(username, currentPassword, newPassword);
+          const userServiceDB = require('./services/userServiceDB');
+          await userServiceDB.changePassword(username, currentPassword, newPassword);
           res.json({ success: true, message: '密码修改成功' });
       } catch (error) {
           logger.error(`用户 ${username} 修改密码失败:`, error);

+ 0 - 1
hubcmdui/config/menu.json

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

+ 0 - 29
hubcmdui/data/config.json

@@ -1,29 +0,0 @@
-{
-  "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": "github.dqzboy.Docker-Proxy"
-}

+ 390 - 0
hubcmdui/database/database.js

@@ -0,0 +1,390 @@
+/**
+ * SQLite 数据库管理模块
+ */
+const sqlite3 = require('sqlite3').verbose();
+const path = require('path');
+const fs = require('fs').promises;
+const logger = require('../logger');
+const bcrypt = require('bcrypt');
+
+// 数据库文件路径
+const DB_PATH = path.join(__dirname, '../data/app.db');
+
+class Database {
+  constructor() {
+    this.db = null;
+  }
+
+  /**
+   * 初始化数据库连接
+   */
+  async connect() {
+    try {
+      // 确保数据目录存在
+      const dbDir = path.dirname(DB_PATH);
+      await fs.mkdir(dbDir, { recursive: true });
+
+      return new Promise((resolve, reject) => {
+        this.db = new sqlite3.Database(DB_PATH, (err) => {
+          if (err) {
+            logger.error('数据库连接失败:', err);
+            reject(err);
+          } else {
+            logger.info('SQLite 数据库连接成功');
+            resolve();
+          }
+        });
+      });
+    } catch (error) {
+      logger.error('初始化数据库失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 创建数据表
+   */
+  async createTables() {
+    const tables = [
+      // 用户表
+      `CREATE TABLE IF NOT EXISTS users (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        username TEXT UNIQUE NOT NULL,
+        password TEXT NOT NULL,
+        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+        login_count INTEGER DEFAULT 0,
+        last_login DATETIME
+      )`,
+
+      // 配置表
+      `CREATE TABLE IF NOT EXISTS configs (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        key TEXT UNIQUE NOT NULL,
+        value TEXT NOT NULL,
+        type TEXT DEFAULT 'string',
+        description TEXT,
+        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+      )`,
+
+      // 文档表
+      `CREATE TABLE IF NOT EXISTS documents (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        doc_id TEXT UNIQUE NOT NULL,
+        title TEXT NOT NULL,
+        content TEXT NOT NULL,
+        published BOOLEAN DEFAULT 0,
+        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+      )`,
+
+      // 系统日志表
+      `CREATE TABLE IF NOT EXISTS system_logs (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        level TEXT NOT NULL,
+        message TEXT NOT NULL,
+        details TEXT,
+        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+      )`,
+
+      // Session表 - 用于存储用户会话
+      `CREATE TABLE IF NOT EXISTS sessions (
+        sid TEXT PRIMARY KEY,
+        sess TEXT NOT NULL,
+        expire DATETIME NOT NULL
+      )`,
+
+      // 菜单项表 - 用于存储导航菜单配置
+      `CREATE TABLE IF NOT EXISTS menu_items (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        text TEXT NOT NULL,
+        link TEXT NOT NULL,
+        new_tab BOOLEAN DEFAULT 0,
+        sort_order INTEGER DEFAULT 0,
+        enabled BOOLEAN DEFAULT 1,
+        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+      )`
+    ];
+
+    for (const sql of tables) {
+      await this.run(sql);
+    }
+
+    logger.info('数据表创建完成');
+  }
+
+  /**
+   * 执行SQL语句
+   */
+  async run(sql, params = []) {
+    return new Promise((resolve, reject) => {
+      this.db.run(sql, params, function(err) {
+        if (err) {
+          logger.error('SQL执行失败:', err);
+          reject(err);
+        } else {
+          resolve({ id: this.lastID, changes: this.changes });
+        }
+      });
+    });
+  }
+
+  /**
+   * 查询单条记录
+   */
+  async get(sql, params = []) {
+    return new Promise((resolve, reject) => {
+      this.db.get(sql, params, (err, row) => {
+        if (err) {
+          logger.error('SQL查询失败:', err);
+          reject(err);
+        } else {
+          resolve(row);
+        }
+      });
+    });
+  }
+
+  /**
+   * 查询多条记录
+   */
+  async all(sql, params = []) {
+    return new Promise((resolve, reject) => {
+      this.db.all(sql, params, (err, rows) => {
+        if (err) {
+          logger.error('SQL查询失败:', err);
+          reject(err);
+        } else {
+          resolve(rows);
+        }
+      });
+    });
+  }
+
+
+
+  /**
+   * 初始化默认管理员用户
+   */
+  async createDefaultAdmin() {
+    try {
+      const adminUser = await this.get('SELECT id FROM users WHERE username = ?', ['root']);
+      
+      if (!adminUser) {
+        const hashedPassword = await bcrypt.hash('admin@123', 10);
+        await this.run(
+          'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
+          ['root', hashedPassword, new Date().toISOString(), 0, null]
+        );
+        logger.info('默认管理员用户创建成功: root/admin@123');
+      }
+    } catch (error) {
+      logger.error('创建默认管理员用户失败:', error);
+    }
+  }
+
+  /**
+   * 创建默认文档
+   */
+  async createDefaultDocuments() {
+    try {
+      const docCount = await this.get('SELECT COUNT(*) as count FROM documents');
+      
+      if (docCount.count === 0) {
+        const defaultDocs = [
+          {
+            doc_id: 'welcome',
+            title: '欢迎使用 Docker 镜像代理加速系统',
+            content: `## 系统介绍
+
+这是一个基于 Nginx 的 Docker 镜像代理加速系统,可以帮助您加速 Docker 镜像的下载和部署。
+
+## 主要功能
+
+- 🚀 **镜像加速**: 提供多个 Docker 镜像仓库的代理加速
+- 🔧 **配置管理**: 简单易用的 Web 管理界面
+- 📊 **监控统计**: 实时监控代理服务状态
+- 📖 **文档管理**: 内置文档系统,方便管理和分享
+
+## 快速开始
+
+1. 访问管理面板进行基础配置
+2. 配置 Docker 客户端使用代理地址
+3. 开始享受加速的镜像下载体验
+
+## 更多信息
+
+如需更多帮助,请查看项目文档或访问 GitHub 仓库。`,
+            published: 1
+          },
+          {
+            doc_id: 'docker-config',
+            title: 'Docker 客户端配置指南',
+            content: `## 配置说明
+
+使用本代理服务需要配置 Docker 客户端的镜像仓库地址。
+
+## Linux/macOS 配置
+
+编辑或创建 \`/etc/docker/daemon.json\` 文件:
+
+\`\`\`json
+{
+  "registry-mirrors": [
+    "http://your-proxy-domain.com"
+  ]
+}
+\`\`\`
+
+重启 Docker 服务:
+\`\`\`bash
+sudo systemctl restart docker
+\`\`\`
+
+## Windows 配置
+
+在 Docker Desktop 设置中:
+1. 打开 Settings -> Docker Engine
+2. 添加配置到 JSON 文件中
+3. 点击 "Apply & Restart"
+
+## 验证配置
+
+运行以下命令验证配置是否生效:
+\`\`\`bash
+docker info
+\`\`\`
+
+在输出中查看 "Registry Mirrors" 部分。`,
+            published: 1
+          }
+        ];
+
+        for (const doc of defaultDocs) {
+          await this.run(
+            'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
+            [doc.doc_id, doc.title, doc.content, doc.published, new Date().toISOString(), new Date().toISOString()]
+          );
+        }
+      }
+    } catch (error) {
+      logger.error('创建默认文档失败:', error);
+    }
+  }
+
+  /**
+   * 检查数据库是否已经初始化
+   */
+  async isInitialized() {
+    try {
+      // 先检查是否有用户表
+      const tableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
+      if (!tableExists) {
+        return false;
+      }
+      
+      // 检查是否有初始化标记
+      const configTableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='configs'");
+      if (configTableExists) {
+        const initFlag = await this.get('SELECT value FROM configs WHERE key = ?', ['db_initialized']);
+        if (initFlag) {
+          return true;
+        }
+      }
+      
+      // 检查是否有用户数据
+      const userCount = await this.get('SELECT COUNT(*) as count FROM users');
+      return userCount && userCount.count > 0;
+    } catch (error) {
+      // 如果查询失败,认为数据库未初始化
+      return false;
+    }
+  }
+
+  /**
+   * 标记数据库已初始化
+   */
+  async markAsInitialized() {
+    try {
+      await this.run(
+        'INSERT OR REPLACE INTO configs (key, value, type, description) VALUES (?, ?, ?, ?)',
+        ['db_initialized', 'true', 'boolean', '数据库初始化标记']
+      );
+      logger.info('数据库已标记为已初始化');
+    } catch (error) {
+      logger.error('标记数据库初始化状态失败:', error);
+    }
+  }
+
+  /**
+   * 关闭数据库连接
+   */
+  async close() {
+    return new Promise((resolve, reject) => {
+      if (this.db) {
+        this.db.close((err) => {
+          if (err) {
+            logger.error('关闭数据库连接失败:', err);
+            reject(err);
+          } else {
+            logger.info('数据库连接已关闭');
+            resolve();
+          }
+        });
+      } else {
+        resolve();
+      }
+    });
+  }
+
+  /**
+   * 清理过期的会话
+   */
+  async cleanExpiredSessions() {
+    try {
+      const result = await this.run(
+        'DELETE FROM sessions WHERE expire < ?',
+        [new Date().toISOString()]
+      );
+      if (result.changes > 0) {
+        logger.info(`清理了 ${result.changes} 个过期会话`);
+      }
+    } catch (error) {
+      logger.error('清理过期会话失败:', error);
+    }
+  }
+
+  /**
+   * 创建默认菜单项
+   */
+  async createDefaultMenuItems() {
+    try {
+      const menuCount = await this.get('SELECT COUNT(*) as count FROM menu_items');
+      
+      if (menuCount.count === 0) {
+        const defaultMenuItems = [
+          { text: '控制台', link: '/admin', new_tab: 0, sort_order: 1 },
+          { text: '镜像搜索', link: '/', new_tab: 0, sort_order: 2 },
+          { text: '文档', link: '/docs', new_tab: 0, sort_order: 3 },
+          { text: 'GitHub', link: 'https://github.com/dqzboy/hubcmdui', new_tab: 1, sort_order: 4 }
+        ];
+
+        for (const item of defaultMenuItems) {
+          await this.run(
+            'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
+            [item.text, item.link, item.new_tab, item.sort_order, 1, new Date().toISOString(), new Date().toISOString()]
+          );
+        }
+      }
+    } catch (error) {
+      logger.error('创建默认菜单项失败:', error);
+    }
+  }
+}
+
+// 创建数据库实例
+const database = new Database();
+
+module.exports = database;

+ 8 - 6
hubcmdui/docker-compose.yaml

@@ -4,13 +4,15 @@ services:
     container_name: hubcmd-ui
     image: dqzboy/hubcmd-ui:latest
     restart: always
+    environment:
+      # HTTP代理配置(可选)
+      #- HTTP_PROXY=http://proxy.example.com:8080
+      #- HTTPS_PROXY=https://proxy.example.com:8080
+      #- NO_PROXY=localhost,127.0.0.1,.local
+      
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock
-      # 配置目录
-      - ./data/config:/app/data
-      # 文档目录
-      - ./data/docs:/app/documentation
-      # 用户数据,需提前在宿主机上创建并把项目users.json内容放入
-      - ./data/user/users.json:/app/users.json
+      # SQLite数据库文件
+      - ./data:/app/data
     ports:
       - 30080:3000

BIN=BIN
hubcmdui/documentation/.DS_Store


+ 0 - 7
hubcmdui/documentation/1743542841590.json

@@ -1,7 +0,0 @@
-{
-  "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-05-10T06:21:33.539Z"
-}

+ 0 - 7
hubcmdui/documentation/1743543376091.json

@@ -1,7 +0,0 @@
-{
-  "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-05-10T06:21:38.920Z"
-}

+ 0 - 7
hubcmdui/documentation/1743543400369.json

@@ -1,7 +0,0 @@
-{
-  "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-05-08T15:16:47.900Z"
-}

+ 1 - 3
hubcmdui/init-dirs.js

@@ -19,14 +19,12 @@ async function ensureDirectoriesExist() {
     path.join(__dirname, 'logs'),
     // 图片目录
     path.join(__dirname, 'web', 'images'),
-    // 数据目录
+    // 数据目录(SQLite数据库文件)
     path.join(__dirname, 'data'),
     // 配置目录
     path.join(__dirname, 'config'),
     // 临时文件目录
     path.join(__dirname, 'temp'),
-    // session 目录
-    path.join(__dirname, 'data', 'sessions'),
     // 文档数据目录
     path.join(__dirname, 'web', 'data', 'documentation')
   ];

+ 7 - 4
hubcmdui/package.json

@@ -4,11 +4,12 @@
   "description": "Docker镜像代理加速系统",
   "main": "server.js",
   "scripts": {
-    "start": "node server.js",
+    "start": "node scripts/auto-setup.js",
+    "start-only": "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 启动服务'"
+    "init": "node scripts/init-complete.js",
+    "setup": "npm install && node scripts/init-complete.js && echo 'SQLite系统安装完成,请使用 npm start 启动服务'"
   },
   "keywords": [
     "docker",
@@ -23,13 +24,15 @@
     "bcrypt": "^5.0.1",
     "body-parser": "^1.20.0",
     "chalk": "^4.1.2",
+    "connect-sqlite3": "^0.9.16",
     "cors": "^2.8.5",
     "dockerode": "^3.3.4",
+    "editor.md": "^1.5.0",
     "express": "^4.21.2",
     "express-session": "^1.18.1",
     "node-cache": "^5.1.2",
     "p-limit": "^4.0.0",
-    "session-file-store": "^1.5.0",
+    "sqlite3": "^5.1.7",
     "systeminformation": "^5.25.11",
     "validator": "^13.7.0",
     "ws": "^8.8.1"

+ 27 - 38
hubcmdui/routes/auth.js

@@ -1,51 +1,48 @@
 /**
- * 认证相关路由
+ * 认证相关路由 - 使用SQLite数据库
  */
 const express = require('express');
 const router = express.Router();
-const bcrypt = require('bcrypt');
-const userService = require('../services/userService');
+const userServiceDB = require('../services/userServiceDB');
 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);
+    // 使用数据库认证
+    const user = await userServiceDB.validateUser(username, password);
     
     if (!user) {
-      logger.warn(`User ${username} not found`);
+      logger.warn(`Login failed for user: ${username}`);
       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: '用户名或密码错误' });
+    // 更新登录信息
+    await userServiceDB.updateUserLoginInfo(username);
+    logger.info(`用户 ${username} 登录成功`);
+
+    // 设置会话
+    req.session.user = { username: user.username };
+    
+    // 确保服务器启动时间已设置
+    if (!global.serverStartTime) {
+      global.serverStartTime = Date.now();
+      logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`);
     }
+    
+    res.json({ 
+      success: true,
+      serverStartTime: global.serverStartTime
+    });
   } catch (error) {
     logger.error('登录失败:', error);
     res.status(500).json({ error: '登录处理失败', details: error.message });
@@ -76,16 +73,9 @@ router.post('/change-password', requireLogin, async (req, res) => {
   }
   
   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' });
-    }
+    // 使用SQLite数据库服务修改密码
+    await userServiceDB.changePassword(req.session.user.username, currentPassword, newPassword);
+    res.json({ success: true });
   } catch (error) {
     logger.error('修改密码失败:', error);
     res.status(500).json({ error: '修改密码失败', details: error.message });
@@ -95,8 +85,7 @@ router.post('/change-password', requireLogin, async (req, res) => {
 // 获取用户信息
 router.get('/user-info', requireLogin, async (req, res) => {
   try {
-    const userService = require('../services/userService');
-    const userStats = await userService.getUserStats(req.session.user.username);
+    const userStats = await userServiceDB.getUserStats(req.session.user.username);
     
     res.json(userStats);
   } catch (error) {

+ 69 - 168
hubcmdui/routes/config.js

@@ -1,63 +1,32 @@
 /**
- * 配置路由模块
+ * 配置路由模块 - 使用SQLite数据库
  */
 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;
-    }
-}
+const configServiceDB = require('../services/configServiceDB');
 
 // 获取配置
 router.get('/config', async (req, res) => {
     try {
-        const config = await ensureConfigFile();
-        res.json(config);
+        const config = await configServiceDB.getConfig();
+        
+        // 如果配置为空,使用默认配置
+        if (!config || Object.keys(config).length === 0) {
+            const defaultConfig = configServiceDB.getDefaultConfig();
+            res.json(defaultConfig);
+        } else {
+            // 合并默认配置和数据库配置,数据库配置优先
+            const defaultConfig = configServiceDB.getDefaultConfig();
+            const mergedConfig = { ...defaultConfig, ...config };
+            res.json(mergedConfig);
+        }
     } catch (error) {
         logger.error('获取配置失败:', error);
-        // 即使失败也返回默认配置
-        res.json(DEFAULT_CONFIG);
+        // 只在真正出错时返回默认配置
+        const defaultConfig = configServiceDB.getDefaultConfig();
+        res.json(defaultConfig);
     }
 });
 
@@ -74,19 +43,8 @@ router.post('/config', async (req, res) => {
             });
         }
         
-        // 读取现有配置
-        let existingConfig;
-        try {
-            existingConfig = await ensureConfigFile();
-        } catch (error) {
-            existingConfig = DEFAULT_CONFIG;
-        }
-        
-        // 合并配置
-        const mergedConfig = { ...existingConfig, ...newConfig };
-        
-        // 保存到文件
-        await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2));
+        // 保存配置到数据库
+        await configServiceDB.saveConfigs(newConfig);
         
         res.json({ success: true, message: '配置已保存' });
     } catch (error) {
@@ -98,127 +56,70 @@ router.post('/config', async (req, res) => {
     }
 });
 
-// 获取监控配置
-router.get('/monitoring-config', async (req, res) => {
-  logger.info('收到监控配置请求');
-  
+// 获取配置 - 兼容旧路由
+router.get('/', async (req, res) => {
   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
-    });
+    const config = await configServiceDB.getConfig();
+    res.json(config);
   } catch (error) {
-    logger.error('获取监控配置失败:', error);
-    res.status(500).json({ error: '获取监控配置失败', details: error.message });
+    logger.error('读取配置失败:', error);
+    const defaultConfig = configServiceDB.getDefaultConfig();
+    res.json(defaultConfig);
   }
 });
 
-// 保存监控配置
-router.post('/monitoring-config', requireLogin, async (req, res) => {
+// 保存配置 - 兼容旧路由
+router.post('/', async (req, res) => {
   try {
-    const { 
-      notificationType, 
-      webhookUrl, 
-      telegramToken, 
-      telegramChatId, 
-      monitorInterval, 
-      isEnabled 
-    } = req.body;
+    const newConfig = 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 不能为空' });
+    if (!newConfig || typeof newConfig !== 'object') {
+        return res.status(400).json({
+            error: '无效的配置数据',
+            details: '配置必须是一个对象'
+        });
     }
     
-    // 保存配置
-    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: '监控配置已保存' });
+    await configServiceDB.saveConfigs(newConfig);
+    res.json({ success: true, message: '配置已保存' });
   } catch (error) {
-    logger.error('保存监控配置失败:', error);
-    res.status(500).json({ error: '保存监控配置失败', details: error.message });
+    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 不能为空' });
+// 获取菜单项配置
+router.get('/menu-items', async (req, res) => {
+    try {
+        const menuItems = await configServiceDB.getMenuItems();
+        res.json(menuItems);
+    } catch (error) {
+        logger.error('获取菜单项失败:', error);
+        res.status(500).json({ error: '获取菜单项失败', details: error.message });
     }
-    
-    if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
-      return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' });
+});
+
+// 保存菜单项配置
+router.post('/menu-items', requireLogin, async (req, res) => {
+    try {
+        const { menuItems } = req.body;
+        
+        if (!Array.isArray(menuItems)) {
+            return res.status(400).json({
+                error: '无效的菜单项数据',
+                details: '菜单项必须是一个数组'
+            });
+        }
+        
+        await configServiceDB.saveMenuItems(menuItems);
+        res.json({ success: true, message: '菜单项配置已保存' });
+    } catch (error) {
+        logger.error('保存菜单项失败:', error);
+        res.status(500).json({ error: '保存菜单项失败', details: error.message });
     }
-    
-    // 构造测试消息
-    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;
+module.exports = router;

+ 43 - 460
hubcmdui/routes/documentation.js

@@ -1,112 +1,53 @@
 /**
- * 文档管理路由
+ * 文档管理路由 - 使用SQLite数据库
  */
 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 documentationServiceDB = require('../services/documentationServiceDB');
 
-// 确保文档目录存在
-const docsDir = path.join(__dirname, '../documentation');
-const metaDir = path.join(docsDir, 'meta');
-
-// 文档文件扩展名
-const FILE_EXTENSION = '.md';
-const META_EXTENSION = '.json';
-
-// 确保目录存在
-async function ensureDirectories() {
+// 获取所有文档列表(管理员)
+router.get('/documents', requireLogin, async (req, res) => {
     try {
-        await fs.mkdir(docsDir, { recursive: true });
-        await fs.mkdir(metaDir, { recursive: true });
+        const documents = await documentationServiceDB.getDocumentationList();
+        res.json(documents);
     } catch (err) {
-        logger.error('创建文档目录失败:', err);
+        logger.error('获取文档列表失败:', err);
+        res.status(500).json({ error: '获取文档列表失败' });
     }
-}
+});
 
-// 读取文档元数据
-async function getDocumentMeta(id) {
-    const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
+// 获取已发布文档列表(公开)
+router.get('/published', async (req, res) => {
     try {
-        const metaContent = await fs.readFile(metaPath, 'utf8');
-        return JSON.parse(metaContent);
+        const documents = await documentationServiceDB.getPublishedDocuments();
+        res.json(documents);
     } catch (err) {
-        // 如果元数据文件不存在,返回默认值
-        return {
-            id,
-            published: false,
-            createdAt: new Date().toISOString(),
-            updatedAt: new Date().toISOString()
-        };
+        logger.error('获取已发布文档列表失败:', err);
+        res.status(500).json({ error: '获取已发布文档列表失败' });
     }
-}
-
-// 保存文档元数据
-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) => {
+// 获取单个文档
+router.get('/documents/:id', 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 { id } = req.params;
+        const document = await documentationServiceDB.getDocument(id);
+        
+        if (!document) {
+            return res.status(404).json({ error: '文档不存在' });
         }
         
-        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
-                });
-            }
+        // 如果文档未发布,需要登录权限
+        if (!document.published && !req.session.user) {
+            return res.status(403).json({ error: '没有权限访问该文档' });
         }
         
-        res.json(documents);
+        res.json(document);
     } catch (err) {
-        logger.error('获取文档列表失败:', err);
-        res.status(500).json({ error: '获取文档列表失败' });
+        logger.error('获取文档失败:', err);
+        res.status(500).json({ error: '获取文档失败' });
     }
 });
 
@@ -120,38 +61,11 @@ router.put('/documents/:id', requireLogin, async (req, res) => {
             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
-        };
-        
+        const document = await documentationServiceDB.saveDocument(id, title, content, published);
         res.json(document);
     } catch (err) {
         logger.error('保存文档失败:', err);
-        res.status(500).json({ error: '保存文档失败', details: err.message });
+        res.status(500).json({ error: '保存文档失败' });
     }
 });
 
@@ -164,42 +78,12 @@ router.post('/documents', requireLogin, async (req, res) => {
             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
-        };
-        
+        const document = await documentationServiceDB.saveDocument(id, title, content, published);
         res.status(201).json(document);
     } catch (err) {
         logger.error('创建文档失败:', err);
-        res.status(500).json({ error: '创建文档失败', details: err.message });
+        res.status(500).json({ error: '创建文档失败' });
     }
 });
 
@@ -207,331 +91,30 @@ router.post('/documents', requireLogin, async (req, res) => {
 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) {
+        const success = await documentationServiceDB.deleteDocument(id);
+        if (!success) {
             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);
+        res.json({ success: true, message: '文档已删除' });
     } catch (err) {
-        logger.error(`更新文档状态 ${req.params.id} 失败:`, err);
-        res.status(500).json({ error: '更新文档状态失败', details: err.message });
+        logger.error('删除文档失败:', err);
+        res.status(500).json({ error: '删除文档失败' });
     }
 });
 
-// 为前端添加获取已发布文档列表的路由
-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) => {
+// 切换文档发布状态
+router.put('/toggle-publish/:id', requireLogin, 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
-        };
         
+        const document = await documentationServiceDB.toggleDocumentPublish(id);
         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.error('切换文档发布状态失败:', err);
+        res.status(500).json({ error: '切换文档发布状态失败' });
     }
 });
 
-// 导出路由
-logger.success('✓ 文档管理路由已加载');
 module.exports = router;

+ 167 - 0
hubcmdui/routes/httpProxy.js

@@ -0,0 +1,167 @@
+/**
+ * HTTP代理管理路由
+ */
+const express = require('express');
+const router = express.Router();
+const logger = require('../logger');
+const { requireLogin } = require('../middleware/auth');
+const httpProxyService = require('../services/httpProxyService');
+
+// 获取代理状态
+router.get('/proxy/status', requireLogin, async (req, res) => {
+  try {
+    const status = httpProxyService.getStatus();
+    res.json(status);
+  } catch (error) {
+    logger.error('获取代理状态失败:', error);
+    res.status(500).json({ 
+      error: '获取代理状态失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 启动代理服务
+router.post('/proxy/start', requireLogin, async (req, res) => {
+  try {
+    const config = req.body;
+    await httpProxyService.start(config);
+    res.json({ 
+      success: true, 
+      message: '代理服务已启动',
+      status: httpProxyService.getStatus()
+    });
+  } catch (error) {
+    logger.error('启动代理服务失败:', error);
+    res.status(500).json({ 
+      error: '启动代理服务失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 停止代理服务
+router.post('/proxy/stop', requireLogin, async (req, res) => {
+  try {
+    await httpProxyService.stop();
+    res.json({ 
+      success: true, 
+      message: '代理服务已停止',
+      status: httpProxyService.getStatus()
+    });
+  } catch (error) {
+    logger.error('停止代理服务失败:', error);
+    res.status(500).json({ 
+      error: '停止代理服务失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 重启代理服务
+router.post('/proxy/restart', requireLogin, async (req, res) => {
+  try {
+    await httpProxyService.stop();
+    await httpProxyService.start(req.body);
+    res.json({ 
+      success: true, 
+      message: '代理服务已重启',
+      status: httpProxyService.getStatus()
+    });
+  } catch (error) {
+    logger.error('重启代理服务失败:', error);
+    res.status(500).json({ 
+      error: '重启代理服务失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 更新代理配置
+router.put('/proxy/config', requireLogin, async (req, res) => {
+  try {
+    const config = req.body;
+    
+    // 验证配置
+    if (config.port && (config.port < 1 || config.port > 65535)) {
+      return res.status(400).json({ error: '端口号必须在1-65535之间' });
+    }
+    
+    if (config.enableAuth && (!config.username || !config.password)) {
+      return res.status(400).json({ error: '启用认证时必须提供用户名和密码' });
+    }
+
+    await httpProxyService.updateConfig(config);
+    res.json({ 
+      success: true, 
+      message: '代理配置已更新',
+      status: httpProxyService.getStatus()
+    });
+  } catch (error) {
+    logger.error('更新代理配置失败:', error);
+    res.status(500).json({ 
+      error: '更新代理配置失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 获取代理配置
+router.get('/proxy/config', requireLogin, async (req, res) => {
+  try {
+    const status = httpProxyService.getStatus();
+    res.json({
+      success: true,
+      config: status.config
+    });
+  } catch (error) {
+    logger.error('获取代理配置失败:', error);
+    res.status(500).json({ 
+      error: '获取代理配置失败', 
+      details: error.message 
+    });
+  }
+});
+
+// 测试代理连接
+router.post('/proxy/test', requireLogin, async (req, res) => {
+  try {
+    const { testUrl = 'http://httpbin.org/ip' } = req.body;
+    const axios = require('axios');
+    const status = httpProxyService.getStatus();
+    
+    if (!status.isRunning) {
+      return res.status(400).json({ error: '代理服务未运行' });
+    }
+
+    // 通过代理测试连接
+    const proxyConfig = {
+      host: status.config.host === '0.0.0.0' ? 'localhost' : status.config.host,
+      port: status.config.port
+    };
+
+    const startTime = Date.now();
+    const response = await axios.get(testUrl, {
+      proxy: proxyConfig,
+      timeout: 10000
+    });
+    const responseTime = Date.now() - startTime;
+
+    res.json({
+      success: true,
+      message: '代理连接测试成功',
+      testUrl,
+      responseTime: `${responseTime}ms`,
+      statusCode: response.status,
+      proxyConfig
+    });
+  } catch (error) {
+    logger.error('代理连接测试失败:', error);
+    res.status(500).json({ 
+      error: '代理连接测试失败', 
+      details: error.message 
+    });
+  }
+});
+
+module.exports = router;

+ 5 - 5
hubcmdui/routes/system.js

@@ -1,5 +1,5 @@
 /**
- * 系统相关路由
+ * 系统相关路由 - 使用SQLite数据库
  */
 const express = require('express');
 const router = express.Router();
@@ -9,7 +9,7 @@ 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 configServiceDB = require('../services/configServiceDB');
 const { execCommand, getSystemInfo } = require('../server-utils');
 const dockerService = require('../services/dockerService');
 const path = require('path');
@@ -115,7 +115,7 @@ async function getSystemStats(req, res) {
 // 获取系统配置 - 修改版本,避免与其他路由冲突
 router.get('/system-config', async (req, res) => {
   try {
-    const config = await configService.getConfig();
+    const config = await configServiceDB.getConfig();
     res.json(config);
   } catch (error) {
     logger.error('读取配置失败:', error);
@@ -129,9 +129,9 @@ router.get('/system-config', async (req, res) => {
 // 保存系统配置 - 修改版本,避免与其他路由冲突
 router.post('/system-config', requireLogin, async (req, res) => {
   try {
-    const currentConfig = await configService.getConfig();
+    const currentConfig = await configServiceDB.getConfig();
     const newConfig = { ...currentConfig, ...req.body };
-    await configService.saveConfig(newConfig);
+    await configServiceDB.saveConfigs(newConfig);
     logger.info('系统配置已更新');
     res.json({ success: true });
   } catch (error) {

+ 117 - 0
hubcmdui/scripts/auto-setup.js

@@ -0,0 +1,117 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+const { isDatabaseReady, getDatabaseStats } = require('../utils/database-checker');
+
+// 检查是否需要安装依赖
+function needsInstall() {
+    const nodeModulesPath = path.join(process.cwd(), 'node_modules');
+    const packageLockPath = path.join(process.cwd(), 'package-lock.json');
+    
+    if (!fs.existsSync(nodeModulesPath)) {
+        return true;
+    }
+    
+    // 检查package.json是否比package-lock.json新
+    const packageJsonPath = path.join(process.cwd(), 'package.json');
+    if (fs.existsSync(packageJsonPath) && fs.existsSync(packageLockPath)) {
+        const packageStat = fs.statSync(packageJsonPath);
+        const lockStat = fs.statSync(packageLockPath);
+        if (packageStat.mtime > lockStat.mtime) {
+            return true;
+        }
+    }
+    
+    return false;
+}
+
+// 检查是否需要初始化数据库
+async function needsInit() {
+    const dataDir = path.join(process.cwd(), 'data');
+    
+    // 如果data目录不存在,需要初始化
+    if (!fs.existsSync(dataDir)) {
+        return true;
+    }
+    
+    // 使用专门的数据库检查器
+    const isReady = await isDatabaseReady();
+    return !isReady;
+}
+
+// 执行命令并显示输出
+function runCommand(command, description) {
+    console.log(`\n🔄 ${description}...`);
+    try {
+        execSync(command, { stdio: 'inherit', cwd: process.cwd() });
+        console.log(`✅ ${description}完成`);
+        return true;
+    } catch (error) {
+        console.error(`❌ ${description}失败:`, error.message);
+        return false;
+    }
+}
+
+async function autoSetup() {
+    console.log('🚀 HubCmdUI 自动设置检查...\n');
+    
+    let needsSetup = false;
+    
+    // 检查是否需要安装依赖
+    if (needsInstall()) {
+        console.log('📦 检测到需要安装依赖包');
+        needsSetup = true;
+        
+        if (!runCommand('npm install', '安装依赖包')) {
+            process.exit(1);
+        }
+    } else {
+        console.log('✅ 依赖包已安装');
+    }
+    
+    // 检查是否需要初始化
+    const needsInitialization = await needsInit();
+    if (needsInitialization) {
+        console.log('🗄️ 检测到需要初始化数据库');
+        needsSetup = true;
+        
+        if (!runCommand('node scripts/init-complete.js', '初始化SQLite数据库')) {
+            process.exit(1);
+        }
+    } else {
+        console.log('✅ 数据库已初始化');
+    }
+    
+    if (needsSetup) {
+        console.log('\n🎉 系统设置完成!正在启动服务...\n');
+    } else {
+        console.log('\n🎯 系统已就绪,正在启动服务...\n');
+    }
+    
+    // 启动服务器
+    console.log('🌐 启动 HubCmdUI 服务器...');
+    console.log('📍 访问地址: http://localhost:3000');
+    console.log('🔧 管理面板: http://localhost:3000/admin');
+    console.log('👤 默认账户: root / admin@123\n');
+    
+    // 启动主服务器
+    try {
+        require('../server.js');
+    } catch (error) {
+        console.error('❌ 服务器启动失败:', error.message);
+        console.error('💡 尝试运行: npm run init 重新初始化');
+        process.exit(1);
+    }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+    autoSetup().catch(error => {
+        console.error('❌ 自动设置失败:', error.message);
+        process.exit(1);
+    });
+}
+
+module.exports = { autoSetup, needsInstall, needsInit };

+ 199 - 0
hubcmdui/scripts/init-complete.js

@@ -0,0 +1,199 @@
+#!/usr/bin/env node
+/**
+ * 系统初始化和配置脚本
+ */
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('../logger');
+
+// 颜色输出
+const chalk = require('chalk');
+
+async function initializeSystem() {
+  console.log(chalk.blue('🚀 正在初始化 HubCmdUI 系统...\n'));
+
+  try {
+    // 1. 检查并创建必要目录
+    console.log(chalk.yellow('📁 创建必要目录...'));
+    await createDirectories();
+
+    // 2. 检查数据库是否已初始化
+    const database = require('../database/database');
+    try {
+      await database.connect();
+      const isInitialized = await database.isInitialized();
+      
+      if (isInitialized) {
+        console.log(chalk.green('  ✓ 数据库已初始化,跳过初始化步骤'));
+        console.log(chalk.green('\n✅ 系统检查完成!'));
+        console.log(chalk.cyan('💡 使用 npm start 启动服务'));
+        console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000'));
+        return;
+      }
+    } catch (error) {
+      // 数据库连接失败,继续初始化流程
+    }
+
+    // 3. 检查配置文件
+    console.log(chalk.yellow('⚙️  检查配置文件...'));
+    await checkConfigFiles();
+
+    // 4. 询问用户是否要启用SQLite
+    const useDatabase = await askUserChoice();
+
+    if (useDatabase) {
+      // 5. 迁移数据到SQLite
+      console.log(chalk.yellow('📊 初始化SQLite数据库...'));
+      await initializeSQLite();
+
+      // 6. 设置环境变量
+      console.log(chalk.yellow('🔧 配置数据库模式...'));
+      await setDatabaseMode(true);
+    } else {
+      console.log(chalk.yellow('📁 使用文件存储模式...'));
+      await setDatabaseMode(false);
+    }
+
+    // 7. 创建默认用户
+    console.log(chalk.yellow('👤 创建默认用户...'));
+    await createDefaultUser();
+
+    // 8. 配置HTTP代理
+    console.log(chalk.yellow('🌐 配置HTTP代理服务...'));
+    await configureHttpProxy();
+
+    console.log(chalk.green('\n✅ 系统初始化完成!'));
+    console.log(chalk.cyan('💡 使用 npm start 启动服务'));
+    console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000'));
+    console.log(chalk.cyan('👤 默认用户: root / admin@123'));
+
+  } catch (error) {
+    console.error(chalk.red('❌ 初始化失败:'), error.message);
+    process.exit(1);
+  }
+}
+
+/**
+ * 创建必要目录
+ */
+async function createDirectories() {
+  const dirs = [
+    'data',          // 数据库文件目录
+    'documentation', // 文档目录(静态文件)
+    'logs',          // 日志目录
+    'temp'           // 临时文件目录
+  ];
+
+  for (const dir of dirs) {
+    const dirPath = path.join(__dirname, '..', dir);
+    try {
+      await fs.mkdir(dirPath, { recursive: true });
+    } catch (error) {
+      if (error.code !== 'EEXIST') {
+        throw error;
+      }
+    }
+  }
+  console.log(chalk.green('  ✓ 目录创建完成'));
+}
+
+/**
+ * 检查配置文件 - 简化版,不再创建config.json
+ */
+async function checkConfigFiles() {
+  console.log(chalk.green('  ✓ 使用SQLite数据库存储配置'));
+}
+
+/**
+ * 询问用户选择
+ */
+async function askUserChoice() {
+  // 简化处理,默认使用SQLite
+  const useDatabase = process.env.USE_SQLITE !== 'false';
+  
+  if (useDatabase) {
+    console.log(chalk.green('  ✓ 将使用SQLite数据库存储'));
+  } else {
+    console.log(chalk.yellow('  ⚠ 将使用文件存储模式'));
+  }
+  
+  return useDatabase;
+}
+
+/**
+ * 初始化SQLite数据库
+ */
+async function initializeSQLite() {
+  try {
+    const database = require('../database/database');
+    await database.connect();
+    await database.createTables();
+    
+    // 初始化数据库(创建默认数据)
+    await database.createDefaultAdmin();
+    await database.createDefaultDocuments();
+    await database.createDefaultMenuItems();
+    
+    // 初始化默认配置
+    const configServiceDB = require('../services/configServiceDB');
+    await configServiceDB.initializeDefaultConfig();
+    
+    // 标记数据库已初始化
+    await database.markAsInitialized();
+    
+    console.log(chalk.green('  ✓ SQLite数据库初始化完成'));
+  } catch (error) {
+    console.log(chalk.red('  ❌ SQLite初始化失败:'), error.message);
+    throw error;
+  }
+}
+
+/**
+ * 设置数据库模式
+ */
+async function setDatabaseMode(useDatabase) {
+  const envPath = path.join(__dirname, '../.env');
+  const envContent = `# 数据库配置
+USE_DATABASE=${useDatabase}
+AUTO_MIGRATE=true
+
+# HTTP代理配置
+PROXY_PORT=8080
+PROXY_HOST=0.0.0.0
+`;
+
+  await fs.writeFile(envPath, envContent);
+  console.log(chalk.green(`  ✓ 数据库模式已设置为: ${useDatabase ? 'SQLite' : '文件存储'}`));
+}
+
+/**
+ * 创建默认用户 - 简化版,数据库已处理
+ */
+async function createDefaultUser() {
+  console.log(chalk.green('  ✓ 默认管理员用户由数据库处理 (root/admin@123)'));
+}
+
+/**
+ * 配置HTTP代理服务信息
+ */
+async function configureHttpProxy() {
+  try {
+    console.log(chalk.green('  ✓ HTTP代理服务需要通过环境变量配置'));
+    console.log(chalk.cyan('    配置方式: 设置 PROXY_PORT 和 PROXY_HOST 环境变量'));
+    console.log(chalk.cyan('    示例: PROXY_PORT=8080 PROXY_HOST=0.0.0.0 npm start'));
+  } catch (error) {
+    console.log(chalk.yellow('  ⚠ HTTP代理服务配置提示显示失败'));
+  }
+}
+
+// 如果直接运行此脚本
+if (require.main === module) {
+  initializeSystem().then(() => {
+    process.exit(0);
+  }).catch((error) => {
+    console.error(chalk.red('初始化失败:'), error);
+    process.exit(1);
+  });
+}
+
+module.exports = { initializeSystem };

+ 92 - 0
hubcmdui/scripts/init-database.js

@@ -0,0 +1,92 @@
+/**
+ * 数据库初始化脚本
+ */
+const database = require('../database/database');
+const userServiceDB = require('../services/userServiceDB');
+const configServiceDB = require('../services/configServiceDB');
+const logger = require('../logger');
+
+async function initializeDatabase() {
+  try {
+    logger.info('开始初始化数据库...');
+
+    // 连接数据库
+    await database.connect();
+
+    // 检查数据库是否已经初始化
+    const isInitialized = await database.isInitialized();
+    if (isInitialized) {
+      logger.info('数据库已经初始化,跳过重复初始化');
+      return;
+    }
+
+    // 创建数据表
+    await database.createTables();
+
+    // 创建默认管理员用户(如果不存在)
+    await database.createDefaultAdmin();
+
+    // 创建默认文档
+    await database.createDefaultDocuments();
+
+    // 初始化默认配置
+    await configServiceDB.initializeDefaultConfig();
+
+    // 标记数据库已初始化
+    await database.markAsInitialized();
+
+    logger.info('数据库初始化完成!');
+    
+    // 显示数据库信息
+    const userCount = await database.get('SELECT COUNT(*) as count FROM users');
+    const configCount = await database.get('SELECT COUNT(*) as count FROM configs');
+    const docCount = await database.get('SELECT COUNT(*) as count FROM documents');
+    
+    logger.info(`数据库统计:`);
+    logger.info(`  用户数量: ${userCount.count}`);
+    logger.info(`  配置项数量: ${configCount.count}`);
+    logger.info(`  文档数量: ${docCount.count}`);
+
+  } catch (error) {
+    logger.error('数据库初始化失败:', error);
+    process.exit(1);
+  }
+}
+
+/**
+ * 检查数据库是否已经初始化
+ */
+async function checkDatabaseInitialized() {
+  try {
+    // 检查用户表是否有数据
+    const userCount = await database.get('SELECT COUNT(*) as count FROM users');
+    if (userCount && userCount.count > 0) {
+      return true;
+    }
+    
+    // 检查配置表是否有数据
+    const configCount = await database.get('SELECT COUNT(*) as count FROM configs');
+    if (configCount && configCount.count > 0) {
+      return true;
+    }
+    
+    return false;
+  } catch (error) {
+    // 如果查询失败,认为数据库未初始化
+    return false;
+  }
+}
+
+// 如果直接运行此脚本,则执行初始化
+if (require.main === module) {
+  initializeDatabase().then(() => {
+    process.exit(0);
+  }).catch((error) => {
+    logger.error('初始化过程出错:', error);
+    process.exit(1);
+  });
+}
+
+module.exports = {
+  initializeDatabase
+};

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

@@ -1,315 +0,0 @@
-/**
- * 系统初始化脚本 - 首次运行时执行
- */
-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
-};

+ 45 - 7
hubcmdui/server.js

@@ -15,7 +15,9 @@ const { gracefulShutdown } = require('./cleanup');
 const os = require('os');
 const { requireLogin } = require('./middleware/auth');
 const compatibilityLayer = require('./compatibility-layer');
-const initSystem = require('./scripts/init-system');
+const { initializeDatabase } = require('./scripts/init-database');
+const database = require('./database/database');
+const httpProxyService = require('./services/httpProxyService');
 
 // 设置日志级别 (默认INFO, 可通过环境变量设置)
 const logLevel = process.env.LOG_LEVEL || 'WARN';
@@ -146,6 +148,15 @@ async function startServer() {
     logger.info(`服务器已启动并监听端口 ${PORT}`);
     
     try {
+      // 初始化数据库
+      try {
+        await initializeDatabase();
+        logger.success('数据库初始化完成');
+      } catch (dbError) {
+        logger.error('数据库初始化失败:', dbError);
+        logger.warn('将使用文件存储作为备用方案');
+      }
+      
       // 确保目录存在
       await ensureDirectoriesExist();
       logger.success('系统目录初始化完成');
@@ -153,15 +164,42 @@ async function startServer() {
       // 下载必要资源
       await downloadImages();
       logger.success('资源下载完成');
+
+      // 默认使用SQLite数据库模式
+      try {
+        logger.info('正在检查SQLite数据库...');
+        const { isDatabaseReady } = require('./utils/database-checker');
+        const dbReady = await isDatabaseReady();
+        
+        if (!dbReady) {
+          logger.warn('数据库未完全初始化,正在初始化...');
+          await initializeDatabase();
+        } else {
+          logger.info('SQLite数据库已就绪');
+        }
+        
+        logger.success('SQLite数据库初始化完成');
+      } catch (dbError) {
+        logger.error('SQLite数据库初始化失败:', dbError.message);
+        throw dbError; // 数据库初始化失败时直接退出
+      }
       
-      // 初始化系统
+      // 初始化系统配置
       try {
-        const { initialize } = require('./scripts/init-system');
-        await initialize();
-        logger.success('系统初始化完成');
+        // 系统配置已在数据库初始化时完成
+        logger.info('系统配置初始化完成');
       } catch (initError) {
-        logger.warn('系统初始化遇到问题:', initError.message);
-        logger.warn('某些功能可能无法正常工作');
+        logger.warn('系统配置初始化遇到问题:', initError.message);
+      }
+      
+      // 初始化HTTP代理服务
+      try {
+        await httpProxyService.loadConfig();
+        // 检查环境变量并自动启动代理
+        await httpProxyService.checkEnvironmentAndAutoStart();
+        logger.success('HTTP代理服务配置已加载');
+      } catch (proxyError) {
+        logger.warn('HTTP代理服务初始化失败:', proxyError.message);
       }
       
       // 尝试启动监控

+ 0 - 47
hubcmdui/services/configService.js

@@ -1,47 +0,0 @@
-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
-};

+ 233 - 0
hubcmdui/services/configServiceDB.js

@@ -0,0 +1,233 @@
+/**
+ * 基于SQLite的配置服务模块
+ */
+const logger = require('../logger');
+const database = require('../database/database');
+
+class ConfigServiceDB {
+  /**
+   * 获取配置项
+   */
+  async getConfig(key = null) {
+    try {
+      if (key) {
+        const config = await database.get('SELECT * FROM configs WHERE key = ?', [key]);
+        if (config) {
+          return JSON.parse(config.value);
+        }
+        return null;
+      } else {
+        // 获取所有配置
+        const configs = await database.all('SELECT * FROM configs');
+        const result = {};
+        for (const config of configs) {
+          result[config.key] = JSON.parse(config.value);
+        }
+        return result;
+      }
+    } catch (error) {
+      logger.error('获取配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 保存配置项
+   */
+  async saveConfig(key, value, description = null) {
+    try {
+      const valueString = JSON.stringify(value);
+      const valueType = typeof value;
+      
+      const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]);
+      
+      if (existingConfig) {
+        // 更新现有配置
+        await database.run(
+          'UPDATE configs SET value = ?, type = ?, description = ?, updated_at = ? WHERE key = ?',
+          [valueString, valueType, description, new Date().toISOString(), key]
+        );
+      } else {
+        // 创建新配置
+        await database.run(
+          'INSERT INTO configs (key, value, type, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
+          [key, valueString, valueType, description, new Date().toISOString(), new Date().toISOString()]
+        );
+      }
+      
+      // 移除详细的配置保存日志,减少日志噪音
+    } catch (error) {
+      logger.error('保存配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 批量保存配置
+   */
+  async saveConfigs(configs) {
+    try {
+      const configCount = Object.keys(configs).length;
+      for (const [key, value] of Object.entries(configs)) {
+        await this.saveConfig(key, value);
+      }
+      logger.info(`批量保存配置完成,共 ${configCount} 项配置`);
+    } catch (error) {
+      logger.error('批量保存配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 删除配置项
+   */
+  async deleteConfig(key) {
+    try {
+      await database.run('DELETE FROM configs WHERE key = ?', [key]);
+      // 删除配置时仍保留日志,因为这是重要操作
+      logger.info(`配置 ${key} 已删除`);
+    } catch (error) {
+      logger.error('删除配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取系统默认配置
+   */
+  getDefaultConfig() {
+    return {
+      theme: 'light',
+      language: 'zh_CN',
+      notifications: true,
+      autoRefresh: true,
+      refreshInterval: 30000,
+      dockerHost: 'localhost',
+      dockerPort: 2375,
+      useHttps: false,
+      proxyDomain: 'registry-1.docker.io',
+      logo: '',
+      menuItems: [
+        {
+          text: "首页",
+          link: "/",
+          newTab: false
+        },
+        {
+          text: "文档",
+          link: "https://dqzboy.github.io/docs/",
+          newTab: true
+        },
+        {
+          text: "推广",
+          link: "https://dqzboy.github.io/proxyui/zanzhu",
+          newTab: true
+        },
+        {
+          text: "GitHub",
+          link: "https://github.com/dqzboy/Docker-Proxy",
+          newTab: true
+        }
+      ],
+      monitoringConfig: {
+        notificationType: 'wechat',
+        webhookUrl: '',
+        telegramToken: '',
+        telegramChatId: '',
+        monitorInterval: 60,
+        isEnabled: false
+      }
+    };
+  }
+
+  /**
+   * 初始化默认配置
+   */
+  async initializeDefaultConfig() {
+    try {
+      const defaultConfig = this.getDefaultConfig();
+      let newConfigCount = 0;
+      
+      for (const [key, value] of Object.entries(defaultConfig)) {
+        const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]);
+        if (!existingConfig) {
+          await this.saveConfig(key, value, `默认${key}配置`);
+          newConfigCount++;
+        }
+      }
+    } catch (error) {
+      logger.error('初始化默认配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取监控配置
+   */
+  async getMonitoringConfig() {
+    try {
+      return await this.getConfig('monitoringConfig') || this.getDefaultConfig().monitoringConfig;
+    } catch (error) {
+      logger.error('获取监控配置失败:', error);
+      return this.getDefaultConfig().monitoringConfig;
+    }
+  }
+
+  /**
+   * 保存监控配置
+   */
+  async saveMonitoringConfig(config) {
+    try {
+      await this.saveConfig('monitoringConfig', config, '监控系统配置');
+    } catch (error) {
+      logger.error('保存监控配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取菜单项配置
+   */
+  async getMenuItems() {
+    try {
+      const menuItems = await database.all(
+        'SELECT text, link, new_tab, sort_order, enabled FROM menu_items WHERE enabled = 1 ORDER BY sort_order'
+      );
+      
+      return menuItems.map(item => ({
+        text: item.text,
+        link: item.link,
+        newTab: Boolean(item.new_tab)
+      }));
+    } catch (error) {
+      logger.error('获取菜单项失败:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 保存菜单项配置
+   */
+  async saveMenuItems(menuItems) {
+    try {
+      // 先清空现有菜单项
+      await database.run('DELETE FROM menu_items');
+      
+      // 插入新的菜单项
+      for (let i = 0; i < menuItems.length; i++) {
+        const item = menuItems[i];
+        await database.run(
+          'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
+          [item.text, item.link, item.newTab ? 1 : 0, i + 1, 1, new Date().toISOString(), new Date().toISOString()]
+        );
+      }
+      
+      logger.info('菜单项配置保存成功');
+    } catch (error) {
+      logger.error('保存菜单项失败:', error);
+      throw error;
+    }
+  }
+}
+
+module.exports = new ConfigServiceDB();

+ 0 - 324
hubcmdui/services/documentationService.js

@@ -1,324 +0,0 @@
-/**
- * 文档服务模块 - 处理文档管理功能
- */
-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
-};

+ 242 - 0
hubcmdui/services/documentationServiceDB.js

@@ -0,0 +1,242 @@
+/**
+ * 基于SQLite的文档服务模块
+ */
+const logger = require('../logger');
+const database = require('../database/database');
+
+class DocumentationServiceDB {
+  /**
+   * 获取文档列表
+   */
+  async getDocumentationList() {
+    try {
+      const documents = await database.all(
+        'SELECT doc_id, title, published, created_at, updated_at FROM documents ORDER BY updated_at DESC'
+      );
+      
+      return documents.map(doc => ({
+        id: doc.doc_id,
+        title: doc.title,
+        published: Boolean(doc.published),
+        createdAt: doc.created_at,
+        updatedAt: doc.updated_at
+      }));
+    } catch (error) {
+      logger.error('获取文档列表失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取已发布文档列表
+   */
+  async getPublishedDocuments() {
+    try {
+      const documents = await database.all(
+        'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE published = 1 ORDER BY updated_at DESC'
+      );
+      
+      return documents.map(doc => ({
+        id: doc.doc_id,
+        title: doc.title,
+        published: Boolean(doc.published),
+        createdAt: doc.created_at,
+        updatedAt: doc.updated_at
+      }));
+    } catch (error) {
+      logger.error('获取已发布文档列表失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取单个文档
+   */
+  async getDocument(docId) {
+    try {
+      const document = await database.get(
+        'SELECT * FROM documents WHERE doc_id = ?',
+        [docId]
+      );
+      
+      if (!document) {
+        throw new Error(`文档 ${docId} 不存在`);
+      }
+      
+      return {
+        id: document.doc_id,
+        title: document.title,
+        content: document.content,
+        published: Boolean(document.published),
+        createdAt: document.created_at,
+        updatedAt: document.updated_at
+      };
+    } catch (error) {
+      logger.error(`获取文档 ${docId} 失败:`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * 保存文档
+   */
+  async saveDocument(docId, title, content, published = false) {
+    try {
+      const id = docId || Date.now().toString();
+      const now = new Date().toISOString();
+      
+      const existingDoc = await database.get(
+        'SELECT id FROM documents WHERE doc_id = ?',
+        [id]
+      );
+      
+      if (existingDoc) {
+        // 更新现有文档
+        await database.run(
+          'UPDATE documents SET title = ?, content = ?, published = ?, updated_at = ? WHERE doc_id = ?',
+          [title, content, published ? 1 : 0, now, id]
+        );
+        logger.info(`文档 ${id} 已更新`);
+      } else {
+        // 创建新文档
+        await database.run(
+          'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
+          [id, title, content, published ? 1 : 0, now, now]
+        );
+        logger.info(`文档 ${id} 已创建`);
+      }
+      
+      return {
+        id,
+        title,
+        content,
+        published: Boolean(published),
+        createdAt: existingDoc ? existingDoc.created_at : now,
+        updatedAt: now
+      };
+    } catch (error) {
+      logger.error('保存文档失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 删除文档
+   */
+  async deleteDocument(docId) {
+    try {
+      const result = await database.run(
+        'DELETE FROM documents WHERE doc_id = ?',
+        [docId]
+      );
+      
+      if (result.changes === 0) {
+        throw new Error(`文档 ${docId} 不存在`);
+      }
+      
+      logger.info(`文档 ${docId} 已删除`);
+      return true;
+    } catch (error) {
+      logger.error(`删除文档 ${docId} 失败:`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * 切换文档发布状态
+   */
+  async toggleDocumentPublish(docId) {
+    try {
+      const document = await this.getDocument(docId);
+      const newPublishedStatus = !document.published;
+      
+      await database.run(
+        'UPDATE documents SET published = ?, updated_at = ? WHERE doc_id = ?',
+        [newPublishedStatus ? 1 : 0, new Date().toISOString(), docId]
+      );
+      
+      logger.info(`文档 ${docId} 发布状态已切换为: ${newPublishedStatus}`);
+      
+      return {
+        ...document,
+        published: newPublishedStatus,
+        updatedAt: new Date().toISOString()
+      };
+    } catch (error) {
+      logger.error(`切换文档 ${docId} 发布状态失败:`, error);
+      throw error;
+    }
+  }
+
+  /**
+   * 批量更新文档发布状态
+   */
+  async batchUpdatePublishStatus(docIds, published) {
+    try {
+      const placeholders = docIds.map(() => '?').join(',');
+      const params = [...docIds, published ? 1 : 0, new Date().toISOString()];
+      
+      const result = await database.run(
+        `UPDATE documents SET published = ?, updated_at = ? WHERE doc_id IN (${placeholders})`,
+        params
+      );
+      
+      logger.info(`批量更新 ${result.changes} 个文档的发布状态`);
+      return result.changes;
+    } catch (error) {
+      logger.error('批量更新文档发布状态失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 搜索文档
+   */
+  async searchDocuments(keyword, publishedOnly = false) {
+    try {
+      let sql = 'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE (title LIKE ? OR content LIKE ?)';
+      const params = [`%${keyword}%`, `%${keyword}%`];
+      
+      if (publishedOnly) {
+        sql += ' AND published = 1';
+      }
+      
+      sql += ' ORDER BY updated_at DESC';
+      
+      const documents = await database.all(sql, params);
+      
+      return documents.map(doc => ({
+        id: doc.doc_id,
+        title: doc.title,
+        published: Boolean(doc.published),
+        createdAt: doc.created_at,
+        updatedAt: doc.updated_at
+      }));
+    } catch (error) {
+      logger.error('搜索文档失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取文档统计信息
+   */
+  async getDocumentStats() {
+    try {
+      const totalCount = await database.get('SELECT COUNT(*) as count FROM documents');
+      const publishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 1');
+      const unpublishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 0');
+      
+      return {
+        total: totalCount.count,
+        published: publishedCount.count,
+        unpublished: unpublishedCount.count
+      };
+    } catch (error) {
+      logger.error('获取文档统计信息失败:', error);
+      throw error;
+    }
+  }
+}
+
+module.exports = new DocumentationServiceDB();

+ 418 - 0
hubcmdui/services/httpProxyService.js

@@ -0,0 +1,418 @@
+/**
+ * HTTP代理服务模块
+ */
+const http = require('http');
+const https = require('https');
+const url = require('url');
+const net = require('net');
+const logger = require('../logger');
+const configServiceDB = require('./configServiceDB');
+
+class HttpProxyService {
+  constructor() {
+    this.proxyServer = null;
+    this.isRunning = false;
+    this.config = {
+      port: 8080,
+      host: '0.0.0.0',
+      enableHttps: true,
+      enableAuth: false,
+      username: '',
+      password: '',
+      allowedHosts: [],
+      blockedHosts: [],
+      logRequests: true
+    };
+  }
+
+  /**
+   * 启动代理服务器
+   */
+  async start(config = {}) {
+    try {
+      this.config = { ...this.config, ...config };
+      
+      if (this.isRunning) {
+        logger.warn('HTTP代理服务器已在运行');
+        return;
+      }
+
+      this.proxyServer = http.createServer();
+      
+      // 处理HTTP请求
+      this.proxyServer.on('request', this.handleHttpRequest.bind(this));
+      
+      // 处理HTTPS CONNECT请求
+      this.proxyServer.on('connect', this.handleHttpsConnect.bind(this));
+      
+      // 错误处理
+      this.proxyServer.on('error', this.handleServerError.bind(this));
+
+      return new Promise((resolve, reject) => {
+        this.proxyServer.listen(this.config.port, this.config.host, (err) => {
+          if (err) {
+            reject(err);
+          } else {
+            this.isRunning = true;
+            logger.info(`HTTP代理服务器已启动,监听 ${this.config.host}:${this.config.port}`);
+            resolve();
+          }
+        });
+      });
+    } catch (error) {
+      logger.error('启动HTTP代理服务器失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 停止代理服务器
+   */
+  async stop() {
+    return new Promise((resolve) => {
+      if (this.proxyServer && this.isRunning) {
+        this.proxyServer.close(() => {
+          this.isRunning = false;
+          logger.info('HTTP代理服务器已停止');
+          resolve();
+        });
+      } else {
+        resolve();
+      }
+    });
+  }
+
+  /**
+   * 处理HTTP请求
+   */
+  handleHttpRequest(clientReq, clientRes) {
+    try {
+      const targetUrl = clientReq.url;
+      const parsedUrl = url.parse(targetUrl);
+      
+      // 记录请求日志
+      if (this.config.logRequests) {
+        logger.info(`HTTP代理请求: ${clientReq.method} ${targetUrl}`);
+      }
+
+      // 认证检查
+      if (this.config.enableAuth && !this.checkAuth(clientReq)) {
+        this.sendAuthRequired(clientRes);
+        return;
+      }
+
+      // 主机检查
+      if (!this.isHostAllowed(parsedUrl.hostname)) {
+        this.sendForbidden(clientRes, '主机不在允许列表中');
+        return;
+      }
+
+      if (this.isHostBlocked(parsedUrl.hostname)) {
+        this.sendForbidden(clientRes, '主机已被阻止');
+        return;
+      }
+
+      // 创建目标请求选项
+      const options = {
+        hostname: parsedUrl.hostname,
+        port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
+        path: parsedUrl.path,
+        method: clientReq.method,
+        headers: { ...clientReq.headers }
+      };
+
+      // 移除代理相关的头部
+      delete options.headers['proxy-connection'];
+      delete options.headers['proxy-authorization'];
+
+      // 选择HTTP或HTTPS
+      const httpModule = parsedUrl.protocol === 'https:' ? https : http;
+
+      // 发送请求到目标服务器
+      const proxyReq = httpModule.request(options, (proxyRes) => {
+        // 复制响应头
+        clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
+        
+        // 管道传输响应数据
+        proxyRes.pipe(clientRes);
+      });
+
+      // 错误处理
+      proxyReq.on('error', (err) => {
+        logger.error('代理请求错误:', err);
+        if (!clientRes.headersSent) {
+          clientRes.writeHead(500, { 'Content-Type': 'text/plain' });
+          clientRes.end('代理服务器错误');
+        }
+      });
+
+      // 管道传输请求数据
+      clientReq.pipe(proxyReq);
+
+    } catch (error) {
+      logger.error('处理HTTP请求失败:', error);
+      if (!clientRes.headersSent) {
+        clientRes.writeHead(500, { 'Content-Type': 'text/plain' });
+        clientRes.end('内部服务器错误');
+      }
+    }
+  }
+
+  /**
+   * 处理HTTPS CONNECT请求
+   */
+  handleHttpsConnect(clientReq, clientSocket, head) {
+    try {
+      const { hostname, port } = this.parseConnectUrl(clientReq.url);
+      
+      // 记录请求日志
+      if (this.config.logRequests) {
+        logger.info(`HTTPS代理请求: CONNECT ${hostname}:${port}`);
+      }
+
+      // 认证检查
+      if (this.config.enableAuth && !this.checkAuth(clientReq)) {
+        clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n\r\n');
+        clientSocket.end();
+        return;
+      }
+
+      // 主机检查
+      if (!this.isHostAllowed(hostname)) {
+        clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+        clientSocket.end();
+        return;
+      }
+
+      if (this.isHostBlocked(hostname)) {
+        clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+        clientSocket.end();
+        return;
+      }
+
+      // 连接到目标服务器
+      const serverSocket = net.connect(port, hostname, () => {
+        // 发送连接成功响应
+        clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
+        
+        // 建立隧道
+        serverSocket.write(head);
+        serverSocket.pipe(clientSocket);
+        clientSocket.pipe(serverSocket);
+      });
+
+      // 错误处理
+      serverSocket.on('error', (err) => {
+        logger.error('服务器连接错误:', err);
+        clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
+        clientSocket.end();
+      });
+
+      clientSocket.on('error', (err) => {
+        logger.error('客户端连接错误:', err);
+        serverSocket.end();
+      });
+
+    } catch (error) {
+      logger.error('处理HTTPS CONNECT请求失败:', error);
+      clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
+      clientSocket.end();
+    }
+  }
+
+  /**
+   * 解析CONNECT请求URL
+   */
+  parseConnectUrl(connectUrl) {
+    const [hostname, port] = connectUrl.split(':');
+    return {
+      hostname,
+      port: parseInt(port) || 443
+    };
+  }
+
+  /**
+   * 检查认证
+   */
+  checkAuth(req) {
+    if (!this.config.enableAuth) {
+      return true;
+    }
+
+    const auth = req.headers['proxy-authorization'];
+    if (!auth) {
+      return false;
+    }
+
+    const [type, credentials] = auth.split(' ');
+    if (type !== 'Basic') {
+      return false;
+    }
+
+    const decoded = Buffer.from(credentials, 'base64').toString();
+    const [username, password] = decoded.split(':');
+
+    return username === this.config.username && password === this.config.password;
+  }
+
+  /**
+   * 检查主机是否允许
+   */
+  isHostAllowed(hostname) {
+    if (this.config.allowedHosts.length === 0) {
+      return true; // 如果没有设置允许列表,则允许所有
+    }
+    
+    return this.config.allowedHosts.some(allowed => {
+      if (allowed.startsWith('*.')) {
+        const domain = allowed.substring(2);
+        return hostname.endsWith(domain);
+      }
+      return hostname === allowed;
+    });
+  }
+
+  /**
+   * 检查主机是否被阻止
+   */
+  isHostBlocked(hostname) {
+    return this.config.blockedHosts.some(blocked => {
+      if (blocked.startsWith('*.')) {
+        const domain = blocked.substring(2);
+        return hostname.endsWith(domain);
+      }
+      return hostname === blocked;
+    });
+  }
+
+  /**
+   * 发送认证要求响应
+   */
+  sendAuthRequired(res) {
+    res.writeHead(407, {
+      'Proxy-Authenticate': 'Basic realm="Proxy"',
+      'Content-Type': 'text/plain'
+    });
+    res.end('需要代理认证');
+  }
+
+  /**
+   * 发送禁止访问响应
+   */
+  sendForbidden(res, message) {
+    res.writeHead(403, { 'Content-Type': 'text/plain' });
+    res.end(message || '禁止访问');
+  }
+
+  /**
+   * 处理服务器错误
+   */
+  handleServerError(error) {
+    logger.error('HTTP代理服务器错误:', error);
+  }
+
+  /**
+   * 获取代理状态
+   */
+  getStatus() {
+    return {
+      isRunning: this.isRunning,
+      config: this.config,
+      port: this.config.port,
+      host: this.config.host
+    };
+  }
+
+  /**
+   * 更新配置
+   */
+  async updateConfig(newConfig) {
+    try {
+      const needRestart = this.isRunning && (
+        newConfig.port !== this.config.port ||
+        newConfig.host !== this.config.host
+      );
+
+      this.config = { ...this.config, ...newConfig };
+
+      if (needRestart) {
+        await this.stop();
+        await this.start();
+        logger.info('HTTP代理服务器配置已更新并重启');
+      } else {
+        logger.info('HTTP代理服务器配置已更新');
+      }
+
+      // 保存配置到数据库
+      await configServiceDB.saveConfig('httpProxyConfig', this.config);
+    } catch (error) {
+      logger.error('更新HTTP代理配置失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 从数据库加载配置
+   */
+  async loadConfig() {
+    try {
+      const savedConfig = await configServiceDB.getConfig('httpProxyConfig');
+      if (savedConfig) {
+        this.config = { ...this.config, ...savedConfig };
+        logger.info('HTTP代理配置已从数据库加载');
+      } else {
+        // 从环境变量加载默认配置
+        this.config = {
+          ...this.config,
+          port: parseInt(process.env.PROXY_PORT) || this.config.port,
+          host: process.env.PROXY_HOST || this.config.host
+        };
+        logger.info('使用默认HTTP代理配置');
+      }
+    } catch (error) {
+      logger.error('加载HTTP代理配置失败:', error);
+      // 使用默认配置
+      logger.info('使用默认HTTP代理配置');
+    }
+  }
+
+  /**
+   * 检查环境变量并自动启动代理
+   */
+  async checkEnvironmentAndAutoStart() {
+    const autoStart = process.env.PROXY_AUTO_START;
+    const proxyPort = process.env.PROXY_PORT;
+    const proxyHost = process.env.PROXY_HOST;
+    const enableAuth = process.env.PROXY_ENABLE_AUTH;
+    const username = process.env.PROXY_USERNAME;
+    const password = process.env.PROXY_PASSWORD;
+
+    // 检查是否应该自动启动代理
+    if (autoStart === 'true' || proxyPort || proxyHost) {
+      logger.info('检测到代理环境变量,尝试自动启动HTTP代理服务...');
+      
+      const envConfig = {};
+      if (proxyPort) envConfig.port = parseInt(proxyPort);
+      if (proxyHost) envConfig.host = proxyHost;
+      if (enableAuth === 'true') {
+        envConfig.enableAuth = true;
+        if (username) envConfig.username = username;
+        if (password) envConfig.password = password;
+      }
+
+      try {
+        await this.start(envConfig);
+        logger.info(`HTTP代理服务已自动启动 - ${envConfig.host || '0.0.0.0'}:${envConfig.port || 8080}`);
+      } catch (error) {
+        logger.warn('自动启动HTTP代理服务失败:', error.message);
+      }
+    } else {
+      logger.info('未检测到代理自动启动环境变量');
+    }
+  }
+}
+
+// 创建单例实例
+const httpProxyService = new HttpProxyService();
+
+module.exports = httpProxyService;

+ 6 - 6
hubcmdui/services/monitoringService.js

@@ -3,7 +3,7 @@
  */
 const axios = require('axios');
 const logger = require('../logger');
-const configService = require('./configService');
+const configServiceDB = require('./configServiceDB');
 const dockerService = require('./dockerService');
 
 // 监控相关状态映射
@@ -15,13 +15,13 @@ let monitoringInterval = null;
 // 更新监控配置
 async function updateMonitoringConfig(config) {
   try {
-    const currentConfig = await configService.getConfig();
+    const currentConfig = await configServiceDB.getConfig();
     currentConfig.monitoringConfig = {
       ...currentConfig.monitoringConfig,
       ...config
     };
     
-    await configService.saveConfig(currentConfig);
+    await configServiceDB.saveConfig(currentConfig);
     
     // 重新启动监控
     await startMonitoring();
@@ -36,7 +36,7 @@ async function updateMonitoringConfig(config) {
 // 启动监控
 async function startMonitoring() {
   try {
-    const config = await configService.getConfig();
+    const config = await configServiceDB.getConfig();
     const { isEnabled, monitorInterval } = config.monitoringConfig || {};
     
     // 如果监控已启用
@@ -308,9 +308,9 @@ async function testNotification(config) {
 
 // 切换监控状态
 async function toggleMonitoring(isEnabled) {
-  const config = await configService.getConfig();
+  const config = await configServiceDB.getConfig();
   config.monitoringConfig.isEnabled = isEnabled;
-  await configService.saveConfig(config);
+  await configServiceDB.saveConfig(config);
   
   return startMonitoring();
 }

+ 0 - 175
hubcmdui/services/userService.js

@@ -1,175 +0,0 @@
-/**
- * 用户服务模块
- */
-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
-};

+ 190 - 0
hubcmdui/services/userServiceDB.js

@@ -0,0 +1,190 @@
+/**
+ * 基于SQLite的用户服务模块
+ */
+const bcrypt = require('bcrypt');
+const logger = require('../logger');
+const database = require('../database/database');
+
+class UserServiceDB {
+  /**
+   * 获取所有用户
+   */
+  async getUsers() {
+    try {
+      const users = await database.all('SELECT * FROM users ORDER BY created_at DESC');
+      return { users };
+    } catch (error) {
+      logger.error('获取用户列表失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 通过用户名获取用户
+   */
+  async getUserByUsername(username) {
+    try {
+      return await database.get('SELECT * FROM users WHERE username = ?', [username]);
+    } catch (error) {
+      logger.error('获取用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 创建新用户
+   */
+  async createUser(username, password) {
+    try {
+      // 检查用户是否已存在
+      const existingUser = await this.getUserByUsername(username);
+      if (existingUser) {
+        throw new Error('用户名已存在');
+      }
+
+      const hashedPassword = await bcrypt.hash(password, 10);
+      const result = await database.run(
+        'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
+        [username, hashedPassword, new Date().toISOString(), 0, null]
+      );
+
+      return { success: true, username, id: result.id };
+    } catch (error) {
+      logger.error('创建用户失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 更新用户登录信息
+   */
+  async updateUserLoginInfo(username) {
+    try {
+      const user = await this.getUserByUsername(username);
+      if (user) {
+        await database.run(
+          'UPDATE users SET login_count = login_count + 1, last_login = ? WHERE username = ?',
+          [new Date().toISOString(), username]
+        );
+      }
+    } catch (error) {
+      logger.error('更新用户登录信息失败:', error);
+    }
+  }
+
+  /**
+   * 获取用户统计信息
+   */
+  async getUserStats(username) {
+    try {
+      const user = await this.getUserByUsername(username);
+      
+      if (!user) {
+        return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
+      }
+
+      // 计算账户年龄
+      let accountAge = '0';
+      if (user.created_at) {
+        const createdDate = new Date(user.created_at);
+        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.last_login) {
+        const lastLoginDate = new Date(user.last_login);
+        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.login_count || 0).toString(),
+        lastLogin,
+        accountAge
+      };
+    } catch (error) {
+      logger.error('获取用户统计信息失败:', error);
+      return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
+    }
+  }
+
+  /**
+   * 修改用户密码
+   */
+  async changePassword(username, currentPassword, newPassword) {
+    try {
+      const user = await this.getUserByUsername(username);
+      
+      if (!user) {
+        throw new Error('用户不存在');
+      }
+
+      // 验证当前密码
+      const isMatch = await bcrypt.compare(currentPassword, user.password);
+      if (!isMatch) {
+        throw new Error('当前密码不正确');
+      }
+
+      // 验证新密码复杂度
+      if (!this.isPasswordComplex(newPassword)) {
+        throw new Error('新密码不符合复杂度要求');
+      }
+
+      // 更新密码
+      const hashedNewPassword = await bcrypt.hash(newPassword, 10);
+      await database.run(
+        'UPDATE users SET password = ?, updated_at = ? WHERE username = ?',
+        [hashedNewPassword, new Date().toISOString(), username]
+      );
+
+      logger.info(`用户 ${username} 密码已成功修改`);
+    } catch (error) {
+      logger.error('修改密码失败:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 验证密码复杂度
+   */
+  isPasswordComplex(password) {
+    // 至少包含1个字母、1个数字和1个特殊字符,长度在8-16位之间
+    const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
+    return passwordRegex.test(password);
+  }
+
+  /**
+   * 验证用户登录
+   */
+  async validateUser(username, password) {
+    try {
+      const user = await this.getUserByUsername(username);
+      if (!user) {
+        return null;
+      }
+
+      const isMatch = await bcrypt.compare(password, user.password);
+      if (isMatch) {
+        return user;
+      }
+      
+      return null;
+    } catch (error) {
+      logger.error('验证用户失败:', error);
+      throw error;
+    }
+  }
+}
+
+module.exports = new UserServiceDB();

+ 0 - 10
hubcmdui/users.json

@@ -1,10 +0,0 @@
-{
-  "users": [
-    {
-      "username": "root",
-      "password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW",
-      "loginCount": 2,
-      "lastLogin": "2025-07-11T02:17:50.457Z"
-    }
-  ]
-}

+ 131 - 0
hubcmdui/utils/database-checker.js

@@ -0,0 +1,131 @@
+/**
+ * 数据库状态检查工具
+ */
+const sqlite3 = require('sqlite3');
+const path = require('path');
+const fs = require('fs');
+
+const DB_PATH = path.join(__dirname, '../data/app.db');
+
+/**
+ * 检查数据库是否已完全初始化
+ */
+async function isDatabaseReady() {
+  return new Promise((resolve) => {
+    // 检查数据库文件是否存在
+    if (!fs.existsSync(DB_PATH)) {
+      resolve(false);
+      return;
+    }
+
+    // 检查文件大小
+    try {
+      const stats = fs.statSync(DB_PATH);
+      if (stats.size < 1024) {
+        resolve(false);
+        return;
+      }
+    } catch (error) {
+      resolve(false);
+      return;
+    }
+
+    // 检查数据库结构和数据
+    const db = new sqlite3.Database(DB_PATH, (err) => {
+      if (err) {
+        resolve(false);
+        return;
+      }
+
+      // 检查必要的表是否存在
+      const requiredTables = ['users', 'configs', 'documents'];
+      let checkedTables = 0;
+      let allTablesReady = true;
+      let tablesWithData = 0;
+
+      requiredTables.forEach(tableName => {
+        db.get(
+          `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
+          [tableName],
+          (err, row) => {
+            if (err || !row) {
+              allTablesReady = false;
+              checkedTables++;
+              checkComplete();
+              return;
+            }
+
+            // 检查表是否有数据(至少用户表和配置表应该有数据)
+            if (tableName === 'users' || tableName === 'configs') {
+              db.get(`SELECT COUNT(*) as count FROM ${tableName}`, (err, countRow) => {
+                if (err || !countRow || countRow.count === 0) {
+                  allTablesReady = false;
+                } else {
+                  tablesWithData++;
+                }
+                checkedTables++;
+                checkComplete();
+              });
+            } else {
+              checkedTables++;
+              checkComplete();
+            }
+          }
+        );
+      });
+
+      function checkComplete() {
+        if (checkedTables === requiredTables.length) {
+          db.close((err) => {
+            // 需要至少用户表和配置表有数据
+            resolve(allTablesReady && tablesWithData >= 2);
+          });
+        }
+      }
+    });
+  });
+}
+
+/**
+ * 获取数据库统计信息
+ */
+async function getDatabaseStats() {
+  return new Promise((resolve) => {
+    if (!fs.existsSync(DB_PATH)) {
+      resolve(null);
+      return;
+    }
+
+    const db = new sqlite3.Database(DB_PATH, (err) => {
+      if (err) {
+        resolve(null);
+        return;
+      }
+
+      const stats = {};
+      let completedQueries = 0;
+      const tables = ['users', 'configs', 'documents'];
+
+      tables.forEach(table => {
+        db.get(`SELECT COUNT(*) as count FROM ${table}`, (err, row) => {
+          completedQueries++;
+          if (!err && row) {
+            stats[table] = row.count;
+          } else {
+            stats[table] = 0;
+          }
+
+          if (completedQueries === tables.length) {
+            db.close();
+            resolve(stats);
+          }
+        });
+      });
+    });
+  });
+}
+
+module.exports = {
+  isDatabaseReady,
+  getDatabaseStats
+};

+ 275 - 23
hubcmdui/web/admin.html

@@ -12,12 +12,24 @@
     <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>
+    
+    <!-- 引入 jQuery (Editor.md 需要) -->
+    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.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>
     
+    <!-- 引入 Editor.md -->
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/codemirror.min.css">
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css">
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/codemirror.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/addon/mode/overlay.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/mode/markdown/markdown.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/mode/gfm/gfm.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
+    
     <!-- 自定义样式 -->
     <link rel="stylesheet" href="css/admin.css">
     <style>
@@ -37,6 +49,11 @@
             transition: all 0.3s ease;
         }
 
+        .sidebar-header {
+            text-align: center;
+            margin-bottom: 1rem;
+        }
+
         /* 文档管理新建文档徽章 */
         .new-badge {
             display: inline-block;
@@ -1481,7 +1498,7 @@
         }
 
         /* 文档编辑器部分 */
-        #editorContainer {
+        #editor #editorContainer {
             margin-top: 2rem;
             border-radius: var(--radius-md);
             background-color: var(--container-bg);
@@ -1493,12 +1510,261 @@
         #documentTitle {
             width: 100%;
             padding: 1rem;
-            margin-bottom: 1rem;
+            margin-bottom: 0;
             border: 1px solid var(--border-color);
-            border-radius: var (--radius-md);;
+            border-radius: var(--radius-md) var(--radius-md) 0 0;
             font-size: 1.1rem;
             font-weight: 500;
-            box-shadow: var(--shadow-sm);
+            box-shadow: none;
+            background-color: var(--container-bg);
+            color: var(--text-primary);
+            border-bottom: 1px solid var(--border-light);
+        }
+
+        #documentTitle:focus {
+            border-color: var(--primary-color);
+            box-shadow: 0 0 0 2px rgba(61, 124, 244, 0.2);
+            outline: none;
+        }
+
+        /* Editor.md 样式覆盖 */
+        #editor #editorContainer .editormd {
+            border: 1px solid var(--border-light) !important;
+            border-top: none !important;
+            border-radius: 0 0 var(--radius-md) var(--radius-md) !important;
+            box-shadow: none !important;
+            margin-top: 0 !important;
+            width: 100% !important;
+            height: auto !important;
+        }
+
+        #editor #editorContainer .editormd-toolbar {
+            background-color: var(--container-bg) !important;
+            border-bottom: 1px solid var(--border-light) !important;
+            padding: 0.5rem !important;
+            width: 100% !important;
+            box-sizing: border-box !important;
+        }
+
+        #editor #editorContainer .editormd-toolbar button {
+            background-color: transparent !important;
+            border: 1px solid var(--border-light) !important;
+            color: var(--text-primary) !important;
+            border-radius: var(--radius-sm) !important;
+            margin: 0 2px !important;
+            padding: 0.4rem 0.6rem !important;
+            transition: all 0.2s ease !important;
+        }
+
+        #editor #editorContainer .editormd-toolbar button:hover {
+            background-color: var(--primary-color) !important;
+            color: white !important;
+            border-color: var(--primary-color) !important;
+        }
+
+        #editor #editorContainer .editormd-toolbar button.active {
+            background-color: var(--primary-color) !important;
+            color: white !important;
+            border-color: var(--primary-color) !important;
+        }
+
+        /* 编辑区域和预览区域等宽等高 */
+        #editor #editorContainer .editormd > .editormd-editor {
+            width: 50% !important;
+            float: left !important;
+            box-sizing: border-box !important;
+        }
+
+        #editor #editorContainer .editormd > .editormd-preview {
+            width: 50% !important;
+            float: right !important;
+            box-sizing: border-box !important;
+        }
+
+        #editor #editorContainer .CodeMirror {
+            background-color: var(--container-bg) !important;
+            color: var(--text-primary) !important;
+            border: none !important;
+            font-family: 'Monaco', 'Consolas', 'Courier New', monospace !important;
+            font-size: 14px !important;
+            line-height: 1.6 !important;
+            height: 500px !important;
+            width: 100% !important;
+            box-sizing: border-box !important;
+        }
+
+        #editor #editorContainer .CodeMirror .CodeMirror-cursor {
+            border-left: 1px solid var(--text-primary) !important;
+        }
+
+        #editor #editorContainer .CodeMirror .CodeMirror-selected {
+            background-color: rgba(61, 124, 244, 0.2) !important;
+        }
+
+        #editor #editorContainer .editormd-preview {
+            background-color: var(--container-bg) !important;
+            color: var(--text-primary) !important;
+            border-left: 1px solid var(--border-light) !important;
+            padding: 1rem !important;
+            height: 500px !important;
+            overflow-y: auto !important;
+            box-sizing: border-box !important;
+        }
+
+        /* 确保容器清除浮动 */
+        #editor #editorContainer .editormd::after {
+            content: "" !important;
+            display: table !important;
+            clear: both !important;
+        }
+
+        #editor #editorContainer .editormd-preview h1,
+        #editor #editorContainer .editormd-preview h2,
+        #editor #editorContainer .editormd-preview h3,
+        #editor #editorContainer .editormd-preview h4,
+        #editor #editorContainer .editormd-preview h5,
+        #editor #editorContainer .editormd-preview h6 {
+            color: var(--text-primary) !important;
+            border-bottom: 1px solid var(--border-light) !important;
+            padding-bottom: 0.5rem !important;
+            margin-bottom: 1rem !important;
+        }
+
+        #editor #editorContainer .editormd-preview pre {
+            background-color: rgba(0, 0, 0, 0.05) !important;
+            border: 1px solid var(--border-light) !important;
+            border-radius: var(--radius-sm) !important;
+            padding: 1rem !important;
+            overflow-x: auto !important;
+        }
+
+        #editor #editorContainer .editormd-preview blockquote {
+            border-left: 4px solid var(--primary-color) !important;
+            background-color: rgba(61, 124, 244, 0.05) !important;
+            margin: 1rem 0 !important;
+            padding: 0.5rem 1rem !important;
+        }
+
+        #editor #editorContainer .editormd-preview table {
+            border-collapse: collapse !important;
+            width: 100% !important;
+            margin: 1rem 0 !important;
+        }
+
+        #editor #editorContainer .editormd-preview table th,
+        #editor #editorContainer .editormd-preview table td {
+            border: 1px solid var(--border-light) !important;
+            padding: 0.5rem 1rem !important;
+            text-align: left !important;
+        }
+
+        #editor #editorContainer .editormd-preview table th {
+            background-color: rgba(61, 124, 244, 0.1) !important;
+            font-weight: 600 !important;
+        }
+
+        /* 编辑器操作按钮样式 */
+        .editor-actions {
+            padding: 1rem !important;
+            background-color: var(--container-bg) !important;
+            border-top: 1px solid var(--border-light) !important;
+            display: flex !important;
+            gap: 0.75rem !important;
+            justify-content: flex-end !important;
+        }
+
+        .editor-actions .btn {
+            padding: 0.75rem 1.5rem !important;
+            border-radius: var(--radius-md) !important;
+            font-weight: 500 !important;
+            transition: all 0.2s ease !important;
+        }
+
+        .editor-actions .btn-primary {
+            background-color: var(--primary-color) !important;
+            color: white !important;
+            border: 1px solid var(--primary-color) !important;
+        }
+
+        .editor-actions .btn-primary:hover {
+            background-color: var(--primary-dark) !important;
+            border-color: var(--primary-dark) !important;
+            transform: translateY(-2px) !important;
+            box-shadow: var(--shadow-md) !important;
+        }
+
+        .editor-actions .btn-secondary {
+            background-color: transparent !important;
+            color: var(--text-secondary) !important;
+            border: 1px solid var(--border-color) !important;
+        }
+
+        .editor-actions .btn-secondary:hover {
+            background-color: var(--text-secondary) !important;
+            color: white !important;
+            transform: translateY(-2px) !important;
+            box-shadow: var(--shadow-sm) !important;
+        }
+
+        /* 响应式 Editor.md 样式 */
+        @media (max-width: 768px) {
+            #editor #editorContainer .editormd {
+                height: auto !important;
+            }
+            
+            /* 移动端编辑区域和预览区域上下布局 */
+            #editor #editorContainer .editormd > .editormd-editor {
+                width: 100% !important;
+                float: none !important;
+                margin-bottom: 1px !important;
+            }
+            
+            #editor #editorContainer .editormd > .editormd-preview {
+                width: 100% !important;
+                float: none !important;
+                border-left: none !important;
+                border-top: 1px solid var(--border-light) !important;
+            }
+            
+            #editor #editorContainer .CodeMirror {
+                height: 300px !important;
+            }
+            
+            #editor #editorContainer .editormd-preview {
+                height: 300px !important;
+            }
+            
+            #editor #editorContainer .editormd-toolbar {
+                padding: 0.3rem !important;
+                flex-wrap: wrap !important;
+            }
+            
+            #editor #editorContainer .editormd-toolbar button {
+                margin: 1px !important;
+                padding: 0.3rem 0.4rem !important;
+                font-size: 0.85rem !important;
+            }
+            
+            .editor-actions {
+                flex-direction: column !important;
+                gap: 0.5rem !important;
+            }
+            
+            .editor-actions .btn {
+                width: 100% !important;
+                text-align: center !important;
+            }
+        }
+
+        /* 中等屏幕适配 */
+        @media (max-width: 1024px) and (min-width: 769px) {
+            #editor #editorContainer .CodeMirror {
+                height: 450px !important;
+            }
+            
+            #editor #editorContainer .editormd-preview {
+                height: 450px !important;
+            }
         }
 
         .password-hint {
@@ -1874,11 +2140,6 @@
             vertical-align: middle;
         }
 
-        /* 网络测试页面美化 */
-        #network-test {
-            /* 可以考虑将整个 #network-test 作为一个卡片,如果它还没有被 .content-section 样式化为卡片的话 */
-        }
-
         /* 直接覆盖#testResults.loading的样式,防止旋转 */
         #network-test #testResults.loading {
             animation: none !important;
@@ -2401,6 +2662,9 @@
     </div>
     <div class="admin-container" id="adminContainer" style="display: none;">
         <div class="sidebar">
+            <div class="sidebar-header">
+                <h2><i class="fas fa-cogs"></i>管理面板</h2>
+            </div>
             <div class="user-profile">
                 <div class="user-avatar">
                     <i class="fas fa-user"></i>
@@ -2414,7 +2678,6 @@
                     <div class="user-action-btn logout" id="logoutBtn">退出登录</div>
                 </div>
             </div>
-            <h2><i class="fas fa-cogs"></i>管理面板</h2>
             <ul class="sidebar-nav">
                 <li data-section="dashboard" class="active">
                     <i class="fas fa-tachometer-alt"></i>控制面板
@@ -2518,17 +2781,6 @@
                             <i class="fas fa-plus"></i> 新建文档
                         </button>
                     </div>
-                    
-                    <div id="editorContainer" style="display: none;">
-                        <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>
-                    </div>
                 </div>  
 
                 <!-- 修改密码部分 -->

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

@@ -394,3 +394,87 @@
 .docker-offline-btn.secondary:hover {
     background-color: #e5e7eb;
 }
+
+/* 简单 Markdown 编辑器样式 */
+.editor-toolbar {
+    background-color: var(--container-bg);
+    border: 1px solid var(--border-color);
+    border-bottom: none;
+    padding: 0.5rem;
+    border-radius: 0.25rem 0.25rem 0 0;
+}
+
+.editor-toolbar .btn {
+    margin-right: 0.25rem;
+    margin-bottom: 0.25rem;
+}
+
+.editor-content {
+    border: 1px solid var(--border-color);
+    border-top: none;
+    padding: 1rem;
+    border-radius: 0 0 0.25rem 0.25rem;
+    background-color: var(--container-bg);
+}
+
+#markdown-editor {
+    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+    border: 1px solid var(--border-color);
+    background-color: var(--background-color);
+    color: var(--text-color);
+    resize: vertical;
+}
+
+.markdown-preview {
+    border: 1px solid var(--border-color);
+    padding: 1rem;
+    background-color: var(--background-color);
+    color: var(--text-color);
+    min-height: 400px;
+    border-radius: 0.25rem;
+    overflow-y: auto;
+}
+
+.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
+.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
+    color: var(--text-color);
+}
+
+.markdown-preview code {
+    background-color: rgba(0, 0, 0, 0.1);
+    padding: 0.125rem 0.25rem;
+    border-radius: 0.125rem;
+    font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
+}
+
+.markdown-preview pre {
+    background-color: rgba(0, 0, 0, 0.1);
+    padding: 1rem;
+    border-radius: 0.25rem;
+    overflow-x: auto;
+}
+
+.markdown-preview blockquote {
+    border-left: 4px solid var(--border-color);
+    padding-left: 1rem;
+    margin-left: 0;
+    color: var(--text-secondary);
+}
+
+.markdown-preview table {
+    border-collapse: collapse;
+    width: 100%;
+    margin: 1rem 0;
+}
+
+.markdown-preview table th,
+.markdown-preview table td {
+    border: 1px solid var(--border-color);
+    padding: 0.5rem;
+    text-align: left;
+}
+
+.markdown-preview table th {
+    background-color: rgba(0, 0, 0, 0.05);
+    font-weight: bold;
+}

+ 500 - 0
hubcmdui/web/document-editor.html

@@ -0,0 +1,500 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>文档编辑器 - HubCmdUI</title>
+    <link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
+    
+    <!-- 引入 Bootstrap CSS -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
+    
+    <!-- 引入 jQuery -->
+    <script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
+    
+    <!-- 引入 Editor.md -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/editormd.min.css">
+    <script src="https://cdn.jsdelivr.net/npm/[email protected]/editormd.min.js"></script>
+    
+    <!-- 引入 SweetAlert2 -->
+    <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
+    
+    <!-- 自定义样式 -->
+    <link rel="stylesheet" href="style.css">
+    <link rel="stylesheet" href="css/admin.css">
+    
+    <style>
+        body {
+            background-color: var(--background-color, #f8f9fa);
+            font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
+            margin: 0;
+            padding: 0;
+            height: 100vh;
+            overflow: hidden;
+        }
+        
+        .editor-container {
+            background-color: var(--container-bg, #ffffff);
+            height: 100vh;
+            display: flex;
+            flex-direction: column;
+            padding: 0;
+            margin: 0;
+        }
+        
+        .editor-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            padding: 1rem 1.5rem;
+            border-bottom: 2px solid var(--border-light, #e9ecef);
+            background-color: var(--container-bg, #ffffff);
+            flex-shrink: 0;
+            z-index: 10;
+        }
+        
+        .editor-title {
+            color: var(--text-primary, #2c3e50);
+            font-size: 1.5rem;
+            font-weight: 600;
+            margin: 0;
+            display: flex;
+            align-items: center;
+            gap: 0.75rem;
+        }
+        
+        .editor-actions {
+            display: flex;
+            gap: 1rem;
+            align-items: center;
+        }
+        
+        .document-title-input {
+            width: 100%;
+            padding: 0.75rem 1.5rem;
+            font-size: 1.1rem;
+            font-weight: 500;
+            border: none;
+            border-bottom: 2px solid var(--border-light, #e9ecef);
+            background-color: var(--container-bg, #ffffff);
+            color: var(--text-primary, #2c3e50);
+            transition: border-color 0.3s ease;
+            flex-shrink: 0;
+        }
+        
+        .document-title-input:focus {
+            outline: none;
+            border-bottom-color: var(--primary-color, #3d7cfa);
+        }
+        
+        .document-title-input::placeholder {
+            color: var(--text-secondary, #6c757d);
+        }
+        
+        /* Editor.md 样式定制 - 铺满全屏 */
+        .editormd {
+            border: none !important;
+            border-radius: 0 !important;
+            flex: 1;
+            height: auto !important;
+        }
+        
+        /* 编辑器容器铺满剩余空间 */
+        #editor-md {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden;
+        }
+        
+        /* 确保 CodeMirror 和预览区域铺满高度 */
+        .editormd .editormd-editor,
+        .editormd .editormd-preview {
+            height: 100% !important;
+        }
+        
+        .CodeMirror {
+            height: 100% !important;
+        }
+        
+        .btn-custom {
+            padding: 0.5rem 1rem;
+            font-weight: 500;
+            border-radius: var(--radius-md, 8px);
+            transition: all 0.3s ease;
+            text-decoration: none;
+            display: inline-flex;
+            align-items: center;
+            gap: 0.5rem;
+            font-size: 0.9rem;
+        }
+        
+        .btn-primary-custom {
+            background-color: var(--primary-color, #3d7cfa);
+            color: white;
+            border: 2px solid var(--primary-color, #3d7cfa);
+        }
+        
+        .btn-primary-custom:hover {
+            background-color: var(--primary-dark, #2c5aa0);
+            border-color: var(--primary-dark, #2c5aa0);
+            transform: translateY(-1px);
+            box-shadow: 0 2px 4px rgba(61, 124, 244, 0.3);
+        }
+        
+        .btn-secondary-custom {
+            background-color: transparent;
+            color: var(--text-secondary, #6c757d);
+            border: 2px solid var(--border-light, #e9ecef);
+        }
+        
+        .btn-secondary-custom:hover {
+            background-color: var(--text-secondary, #6c757d);
+            color: white;
+            border-color: var(--text-secondary, #6c757d);
+        }
+        
+        .btn-success-custom {
+            background-color: var(--success-color, #28a745);
+            color: white;
+            border: 2px solid var(--success-color, #28a745);
+        }
+        
+        .btn-success-custom:hover {
+            background-color: #218838;
+            border-color: #218838;
+            transform: translateY(-1px);
+            box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
+        }
+        
+        /* 响应式设计 */
+        @media (max-width: 768px) {
+            .editor-header {
+                flex-direction: column;
+                gap: 0.5rem;
+                align-items: stretch;
+                padding: 1rem;
+            }
+            
+            .editor-title {
+                font-size: 1.3rem;
+                text-align: center;
+            }
+            
+            .editor-actions {
+                flex-direction: row;
+                gap: 0.5rem;
+                justify-content: center;
+            }
+            
+            .btn-custom {
+                flex: 1;
+                justify-content: center;
+                font-size: 0.85rem;
+                padding: 0.4rem 0.8rem;
+            }
+            
+            .document-title-input {
+                padding: 0.6rem 1rem;
+                font-size: 1rem;
+            }
+        }
+        
+        .loading-overlay {
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background-color: rgba(0, 0, 0, 0.5);
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            z-index: 9999;
+        }
+        
+        .loading-spinner {
+            background-color: white;
+            padding: 2rem;
+            border-radius: var(--radius-lg, 12px);
+            text-align: center;
+            box-shadow: var(--shadow-lg, 0 8px 16px rgba(0, 0, 0, 0.15));
+        }
+        
+        .spinner {
+            border: 3px solid #f3f3f3;
+            border-top: 3px solid var(--primary-color, #3d7cfa);
+            border-radius: 50%;
+            width: 40px;
+            height: 40px;
+            animation: spin 1s linear infinite;
+            margin: 0 auto 1rem;
+        }
+        
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+    </style>
+</head>
+<body>
+    <div class="editor-container">
+        <div class="editor-header">
+            <h1 class="editor-title">
+                <i class="fas fa-edit"></i>
+                <span id="pageTitle">新建文档</span>
+            </h1>
+            <div class="editor-actions">
+                <a href="/admin" class="btn-custom btn-secondary-custom">
+                    <i class="fas fa-arrow-left"></i> 返回管理面板
+                </a>
+                <button type="button" class="btn-custom btn-success-custom" id="saveBtn">
+                    <i class="fas fa-save"></i> 保存文档
+                </button>
+            </div>
+        </div>
+        
+        <input 
+            type="text" 
+            id="documentTitle" 
+            class="document-title-input" 
+            placeholder="请输入文档标题..." 
+            autocomplete="off"
+        >
+        
+        <div id="editor-md">
+            <textarea style="display:none;" id="editorContent"></textarea>
+        </div>
+    </div>
+    
+    <!-- 加载覆盖层 -->
+    <div class="loading-overlay" id="loadingOverlay" style="display: none;">
+        <div class="loading-spinner">
+            <div class="spinner"></div>
+            <p>正在保存文档...</p>
+        </div>
+    </div>
+
+    <script>
+        let editor;
+        let currentDocId = null;
+        
+        // 从 URL 参数获取文档 ID(如果是编辑模式)
+        const urlParams = new URLSearchParams(window.location.search);
+        const docId = urlParams.get('id');
+        
+        $(document).ready(function() {
+            // 初始化 Editor.md
+            initEditor();
+            
+            // 如果有文档 ID,则加载文档
+            if (docId) {
+                loadDocument(docId);
+                document.getElementById('pageTitle').textContent = '编辑文档';
+            }
+            
+            // 绑定保存按钮事件
+            document.getElementById('saveBtn').addEventListener('click', saveDocument);
+            
+            // 绑定键盘快捷键 Ctrl+S
+            document.addEventListener('keydown', function(e) {
+                if (e.ctrlKey && e.key === 's') {
+                    e.preventDefault();
+                    saveDocument();
+                }
+            });
+        });
+        
+        function initEditor() {
+            editor = editormd("editor-md", {
+                width: "100%",
+                height: "100%", // 使用 CSS 的 flex 布局控制高度
+                syncScrolling: "single",
+                placeholder: "在这里编写您的 Markdown 内容...",
+                path: "https://cdn.jsdelivr.net/npm/[email protected]/lib/",
+                // 使用官方默认主题
+                theme: "default",
+                previewTheme: "default", 
+                editorTheme: "default",
+                markdown: "",
+                codeFold: true,
+                saveHTMLToTextarea: true,
+                searchReplace: true,
+                htmlDecode: "style,script,iframe",
+                emoji: true,
+                taskList: true,
+                tocm: true,
+                tex: false,
+                flowChart: false,
+                sequenceDiagram: false,
+                dialogLockScreen: false,
+                dialogShowMask: false,
+                previewCodeHighlight: true,
+                toolbar: true,
+                watch: true,
+                lineNumbers: true,
+                lineWrapping: false,
+                autoCloseTags: true,
+                autoFocus: true,
+                indentUnit: 4,
+                // 使用官方默认工具栏配置
+                onload: function() {
+                    console.log('Editor.md 初始化完成');
+                },
+                onchange: function() {
+                    // 标记内容已修改
+                    markAsModified();
+                }
+            });
+        }
+        
+        function markAsModified() {
+            const saveBtn = document.getElementById('saveBtn');
+            if (!saveBtn.classList.contains('modified')) {
+                saveBtn.classList.add('modified');
+                saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档 *';
+            }
+        }
+        
+        function markAsSaved() {
+            const saveBtn = document.getElementById('saveBtn');
+            saveBtn.classList.remove('modified');
+            saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档';
+        }
+        
+        async function loadDocument(id) {
+            try {
+                const response = await fetch(`/api/documents/${id}`, {
+                    credentials: 'same-origin'
+                });
+                
+                if (!response.ok) {
+                    throw new Error(`加载文档失败: ${response.status}`);
+                }
+                
+                const doc = await response.json();
+                currentDocId = id;
+                
+                // 设置文档标题
+                document.getElementById('documentTitle').value = doc.title || '';
+                
+                // 设置文档内容
+                if (editor && editor.setMarkdown) {
+                    editor.setMarkdown(doc.content || '');
+                }
+                
+                // 更新页面标题
+                document.title = `编辑文档: ${doc.title} - HubCmdUI`;
+                
+            } catch (error) {
+                console.error('加载文档失败:', error);
+                Swal.fire({
+                    icon: 'error',
+                    title: '加载失败',
+                    text: '无法加载文档内容,请检查网络连接或文档是否存在。',
+                    confirmButtonColor: '#3d7cfa'
+                });
+            }
+        }
+        
+        async function saveDocument() {
+            const title = document.getElementById('documentTitle').value.trim();
+            const content = editor ? editor.getMarkdown() : '';
+            
+            if (!title) {
+                Swal.fire({
+                    icon: 'warning',
+                    title: '请输入标题',
+                    text: '文档标题不能为空',
+                    confirmButtonColor: '#3d7cfa'
+                });
+                return;
+            }
+            
+            if (!content.trim()) {
+                Swal.fire({
+                    icon: 'warning',
+                    title: '请输入内容',
+                    text: '文档内容不能为空',
+                    confirmButtonColor: '#3d7cfa'
+                });
+                return;
+            }
+            
+            // 显示加载动画
+            document.getElementById('loadingOverlay').style.display = 'flex';
+            
+            try {
+                const url = currentDocId ? `/api/documents/${currentDocId}` : '/api/documents';
+                const method = currentDocId ? 'PUT' : 'POST';
+                
+                const response = await fetch(url, {
+                    method: method,
+                    headers: {
+                        'Content-Type': 'application/json',
+                    },
+                    credentials: 'same-origin',
+                    body: JSON.stringify({
+                        title: title,
+                        content: content
+                    })
+                });
+                
+                if (!response.ok) {
+                    throw new Error(`保存失败: ${response.status}`);
+                }
+                
+                const result = await response.json();
+                
+                // 如果是新建文档,更新当前文档 ID
+                if (!currentDocId && result.id) {
+                    currentDocId = result.id;
+                    // 更新 URL
+                    window.history.replaceState({}, '', `?id=${result.id}`);
+                    document.getElementById('pageTitle').textContent = '编辑文档';
+                }
+                
+                // 标记为已保存
+                markAsSaved();
+                
+                // 更新页面标题
+                document.title = `编辑文档: ${title} - HubCmdUI`;
+                
+                // 显示成功消息
+                Swal.fire({
+                    icon: 'success',
+                    title: '保存成功',
+                    text: currentDocId ? '文档已更新' : '文档已创建',
+                    timer: 2000,
+                    showConfirmButton: false,
+                    toast: true,
+                    position: 'top-end'
+                });
+                
+            } catch (error) {
+                console.error('保存文档失败:', error);
+                Swal.fire({
+                    icon: 'error',
+                    title: '保存失败',
+                    text: '无法保存文档,请检查网络连接或稍后重试。',
+                    confirmButtonColor: '#3d7cfa'
+                });
+            } finally {
+                // 隐藏加载动画
+                document.getElementById('loadingOverlay').style.display = 'none';
+            }
+        }
+        
+        // 页面卸载前提醒保存
+        window.addEventListener('beforeunload', function(e) {
+            const saveBtn = document.getElementById('saveBtn');
+            if (saveBtn && saveBtn.classList.contains('modified')) {
+                e.preventDefault();
+                e.returnValue = '您有未保存的更改,确定要离开吗?';
+                return e.returnValue;
+            }
+        });
+    </script>
+</body>
+</html>

+ 178 - 21
hubcmdui/web/index.html

@@ -13,7 +13,7 @@
     <header class="header">
         <div class="header-content">
             <a href="/" class="logo-link">
-                <img src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo">
+                <img id="mainLogo" src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo" style="opacity: 0; transition: opacity 0.3s ease;">
             </a>
             <nav class="nav-menu" id="navMenu">
                 <!-- 菜单项通过 JavaScript 动态加载 -->
@@ -1530,7 +1530,11 @@
 
         // 显示指定的文档
         function showDocument(index) {
-            // console.log('显示文档索引:', index);
+            // 清理之前的返回顶部按钮
+            const existingBackToTopBtn = document.querySelector('.back-to-top-btn');
+            if (existingBackToTopBtn) {
+                existingBackToTopBtn.remove();
+            }
             
             if (!window.documentationData || !Array.isArray(window.documentationData)) {
                 console.error('文档数据不可用');
@@ -1673,74 +1677,192 @@
         function renderMarkdownContent(container, doc) {
             if (!container) return;
             
-            // console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
-            
             if (doc.content) {
                 // 使用marked渲染Markdown内容
                 if (window.marked) {
                     try {
+                        // 配置marked选项以获得更好的渲染效果
+                        marked.setOptions({
+                            highlight: function(code, lang) {
+                                // 如果有语法高亮库,可以在这里使用
+                                return code;
+                            },
+                            langPrefix: 'language-',
+                            breaks: true,
+                            gfm: true
+                        });
+
                         const rawHtml = marked.parse(doc.content);
 
                         // 创建一个临时的根元素来容纳和处理已解析的Markdown内容
                         const docFragmentRoot = document.createElement('div');
                         docFragmentRoot.innerHTML = rawHtml;
 
-                        // 在这个临时根元素中查找所有的 <pre> 元素
+                        // 为代码块添加语言标识和复制按钮
                         const preElements = docFragmentRoot.querySelectorAll('pre');
-                        preElements.forEach(preElement => {
+                        preElements.forEach((preElement, index) => {
                             const codeElement = preElement.querySelector('code');
                             let codeToCopy = '';
+                            let language = 'Code';
+                            
                             if (codeElement) {
                                 codeToCopy = codeElement.textContent;
+                                // 尝试从className获取语言信息
+                                const className = codeElement.className;
+                                const langMatch = className.match(/language-(\w+)/);
+                                if (langMatch) {
+                                    language = langMatch[1].toUpperCase();
+                                }
                             } else {
                                 codeToCopy = preElement.textContent;
                             }
 
+                            // 设置语言属性用于CSS显示
+                            preElement.setAttribute('data-language', language);
+
                             if (codeToCopy.trim() !== '') {
                                 const copyButton = document.createElement('button');
-                                copyButton.className = 'copy-btn'; // 应用现有样式
-                                copyButton.textContent = '复制';
-                                copyButton.onclick = function() { // 事件监听器在此处附加到按钮对象
-                                    // console.log('[Tutorial Copy Button] Attempting to copy:', codeToCopy); // 保留此调试日志
+                                copyButton.className = 'copy-btn';
+                                copyButton.innerHTML = '<i class="fas fa-copy"></i> 复制';
+                                copyButton.onclick = function() {
                                     copyToClipboard(codeToCopy, this);
                                 };
-                                preElement.style.position = 'relative';
-                                preElement.appendChild(copyButton); // 按钮被追加到 docFragmentRoot 内的 preElement
+                                preElement.appendChild(copyButton);
                             }
                         });
 
+                        // 为链接添加外部链接图标
+                        const links = docFragmentRoot.querySelectorAll('a');
+                        links.forEach(link => {
+                            const href = link.getAttribute('href');
+                            if (href && (href.startsWith('http') || href.startsWith('https'))) {
+                                link.innerHTML += ' <i class="fas fa-external-link-alt" style="font-size: 0.8em; margin-left: 0.25rem;"></i>';
+                                link.setAttribute('target', '_blank');
+                                link.setAttribute('rel', 'noopener noreferrer');
+                            }
+                        });
+
+                        // 为表格添加响应式包装
+                        const tables = docFragmentRoot.querySelectorAll('table');
+                        tables.forEach(table => {
+                            const wrapper = document.createElement('div');
+                            wrapper.className = 'table-wrapper';
+                            wrapper.style.overflowX = 'auto';
+                            wrapper.style.marginBottom = '1.5rem';
+                            table.parentNode.insertBefore(wrapper, table);
+                            wrapper.appendChild(table);
+                        });
+
                         // 清空页面上的主容器
                         container.innerHTML = '';
 
+                        // 创建文档头部
+                        const docHeader = document.createElement('div');
+                        docHeader.className = 'doc-header';
+                        docHeader.innerHTML = `
+                            <h1>${doc.title || '文档标题'}</h1>
+                            ${doc.description ? `<p class="doc-description">${doc.description}</p>` : ''}
+                        `;
+                        container.appendChild(docHeader);
+
                         // 创建 .doc-content div 并将处理过的文档片段追加进去
                         const docContentDiv = document.createElement('div');
                         docContentDiv.className = 'doc-content';
-                        // 将 docFragmentRoot 的所有子节点移动到 docContentDiv,以避免多余的包裹 div
+                        // 将 docFragmentRoot 的所有子节点移动到 docContentDiv
                         while (docFragmentRoot.firstChild) {
                             docContentDiv.appendChild(docFragmentRoot.firstChild);
                         }
-                        container.appendChild(docContentDiv); // docContentDiv 现在包含带有活动按钮的 PRE 元素
+                        container.appendChild(docContentDiv);
 
                         // 创建并追加 .doc-meta div
                         const docMetaDiv = document.createElement('div');
                         docMetaDiv.className = 'doc-meta';
-                        docMetaDiv.innerHTML = `${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}`;
+                        const updateTime = doc.lastUpdated || doc.updatedAt || doc.updated_at;
+                        if (updateTime) {
+                            const formattedDate = new Date(updateTime).toLocaleDateString('zh-CN', {
+                                year: 'numeric',
+                                month: 'long',
+                                day: 'numeric',
+                                hour: '2-digit',
+                                minute: '2-digit'
+                            });
+                            docMetaDiv.innerHTML = `
+                                <i class="fas fa-clock"></i>
+                                <span>最后更新: ${formattedDate}</span>
+                            `;
+                        }
                         container.appendChild(docMetaDiv);
 
+                        // 添加返回顶部按钮(如果内容很长)
+                        if (docContentDiv.scrollHeight > 1000) {
+                            const backToTopBtn = document.createElement('button');
+                            backToTopBtn.className = 'back-to-top-btn';
+                            backToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
+                            backToTopBtn.style.cssText = `
+                                position: fixed;
+                                bottom: 2rem;
+                                right: 2rem;
+                                width: 3rem;
+                                height: 3rem;
+                                border-radius: 50%;
+                                background: var(--primary-color);
+                                color: white;
+                                border: none;
+                                cursor: pointer;
+                                box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
+                                z-index: 1000;
+                                opacity: 0.8;
+                                transition: all 0.3s ease;
+                            `;
+                            backToTopBtn.onclick = () => {
+                                container.scrollIntoView({ behavior: 'smooth' });
+                            };
+                            backToTopBtn.onmouseenter = () => {
+                                backToTopBtn.style.opacity = '1';
+                                backToTopBtn.style.transform = 'scale(1.1)';
+                            };
+                            backToTopBtn.onmouseleave = () => {
+                                backToTopBtn.style.opacity = '0.8';
+                                backToTopBtn.style.transform = 'scale(1)';
+                            };
+                            document.body.appendChild(backToTopBtn);
+                            
+                            // 当切换文档时清理按钮
+                            container.setAttribute('data-back-to-top', 'true');
+                        }
+
                     } catch (error) {
                         console.error('Markdown解析失败:', error);
-                        // 发生错误时,仍然显示原始Markdown内容 + Meta
-                        container.innerHTML = ` 
-                            <div class="doc-content">${doc.content}</div>
+                        // 发生错误时的降级处理
+                        container.innerHTML = `
+                            <div class="doc-header">
+                                <h1>${doc.title || '文档标题'}</h1>
+                            </div>
+                            <div class="doc-content">
+                                <div class="error-container">
+                                    <i class="fas fa-exclamation-triangle"></i>
+                                    <h3>内容解析失败</h3>
+                                    <p>无法正确解析文档内容,显示原始内容:</p>
+                                    <pre><code>${doc.content}</code></pre>
+                                </div>
+                            </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>
+                    // marked 不可用时的降级处理
+                    container.innerHTML = `
+                        <div class="doc-header">
+                            <h1>${doc.title || '文档标题'}</h1>
+                        </div>
+                        <div class="doc-content">
+                            <div class="markdown-fallback">
+                                <p><em>Markdown 解析器未加载,显示原始内容:</em></p>
+                                <pre><code>${doc.content}</code></pre>
+                            </div>
+                        </div>
                         <div class="doc-meta">
                             ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
                         </div>
@@ -1767,6 +1889,9 @@
         
         // DOMContentLoaded 事件监听器
         document.addEventListener('DOMContentLoaded', function() {
+            // 加载系统配置(包括 logo)
+            loadSystemConfig();
+            
             // 初始化代理域名
             initProxyDomain();
             
@@ -1786,6 +1911,38 @@
             // 统一调用文档加载函数
             loadAndDisplayDocumentation();
         });
+        
+        // 加载系统配置
+        function loadSystemConfig() {
+            fetch('/api/config')
+                .then(response => {
+                    if (response.ok) {
+                        return response.json();
+                    }
+                    // 如果配置加载失败,使用默认配置
+                    return {};
+                })
+                .then(config => {
+                    const logoElement = document.getElementById('mainLogo');
+                    if (logoElement) {
+                        // 如果有自定义logo配置且不为空,则使用自定义logo
+                        if (config.logo && config.logo.trim() !== '') {
+                            logoElement.src = config.logo;
+                        }
+                        // 如果没有配置或为空,保持默认logo不变
+                        // 显示logo(无论是默认还是自定义)
+                        logoElement.style.opacity = '1';
+                    }
+                })
+                .catch(error => {
+                    // 如果出错,也要显示默认logo
+                    console.warn('加载配置失败:', error);
+                    const logoElement = document.getElementById('mainLogo');
+                    if (logoElement) {
+                        logoElement.style.opacity = '1';
+                    }
+                });
+        }
     </script>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
 </body>

+ 5 - 1
hubcmdui/web/js/core.js

@@ -150,6 +150,11 @@ function applySystemConfig(config) {
         document.getElementById('proxyDomain').value = config.proxyDomain;
     }
     
+    // 更新logo配置输入框(管理页面不显示logo图片,只显示配置)
+    if (document.getElementById('logoUrl')) {
+        document.getElementById('logoUrl').value = config.logo || '';
+    }
+    
     // 应用其他配置...
 }
 
@@ -477,7 +482,6 @@ function toggleLoadingState(isLoading, elementId, originalText = null) {
         // 如果按钮文本没有被修改为 "加载中...",则不需要恢复
     }
 }
-
 // 页面加载时初始化
 document.addEventListener('DOMContentLoaded', function() {
     // console.log('DOM已加载,正在初始化应用...');

+ 5 - 401
hubcmdui/web/js/documentManager.js

@@ -4,10 +4,6 @@
 
 // 文档列表
 let documents = [];
-// 当前正在编辑的文档
-let currentDocument = null;
-// Markdown编辑器实例
-let editorMd = null;
 
 // 创建documentManager对象
 const documentManager = {
@@ -179,240 +175,10 @@ const documentManager = {
         }
     },
 
-    // 初始化编辑器
-    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();
-        }
+        // 跳转到专门的文档编辑页面
+        window.open('/document-editor.html', '_blank');
     },
 
     // 渲染文档列表
@@ -504,84 +270,8 @@ const documentManager = {
 
     // 编辑文档
     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) sessionValid = false;
-            } catch (sessionError) { 
-                // console.warn('检查会话状态发生网络错误:', sessionError);
-                // 继续尝试,API调用时会再次处理401
-            }
-            
-            if (!sessionValid) {
-                core.showAlert('您的会话已过期,请重新登录', 'warning');
-                auth.showLoginModal(); // 使用 auth 模块显示登录
-                core.hideLoading();
-                return;
-            }
-            
-            // 不再依赖本地缓存的列表项获取content,始终从API获取完整文档
-            // console.log('始终从API获取完整文档详情进行编辑,ID:', id);
-            
-            const response = await fetch(`/api/documents/${id}`, {
-                headers: { 
-                    'Cache-Control': 'no-cache', // 确保获取最新数据
-                    'X-Requested-With': 'XMLHttpRequest' 
-                },
-                credentials: 'same-origin'
-            });
-
-            if (response.status === 401) {
-                core.showAlert('您的会话已过期或无权限访问此文档,请重新登录', 'warning');
-                auth.showLoginModal();
-                core.hideLoading();
-                return;
-            }
-
-            if (!response.ok) {
-                const errorText = await response.text();
-                // console.error(`获取文档失败 (${response.status}):`, errorText);
-                throw new Error(`获取文档内容失败: ${errorText || response.status}`);
-            }
-
-            const docToEdit = await response.json();
-            currentDocument = docToEdit; // 更新当前编辑的文档对象
-
-            // 确保编辑器已初始化
-            if (!editorMd) {
-                this.initEditor();
-                // 等待编辑器初始化完成后再继续
-                // 使用短延时确保编辑器DOM完全准备好
-                await new Promise(resolve => setTimeout(resolve, 100)); 
-            }
-            
-            if (!editorMd) {
-                core.showAlert('编辑器初始化失败,无法编辑文档。', 'error');
-                core.hideLoading();
-                return;
-            }
-            
-            document.getElementById('documentTitle').value = docToEdit.title || '';
-            editorMd.setMarkdown(docToEdit.content || '');
-            this.showEditor();
-
-        } catch (error) {
-            // console.error('编辑文档时出错:', error);
-            core.showAlert(`加载文档进行编辑失败: ${error.message}`, 'error');
-        } finally {
-            core.hideLoading();
-        }
+        // 跳转到专门的文档编辑页面,并传递文档ID
+        window.open(`/document-editor.html?id=${id}`, '_blank');
     },
 
     // 查看文档
@@ -753,90 +443,4 @@ const documentManager = {
             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' });
-    }
-}
+};

+ 321 - 0
hubcmdui/web/js/httpProxyManager.js

@@ -0,0 +1,321 @@
+/**
+ * HTTP代理管理模块
+ */
+
+const httpProxyManager = {
+    currentConfig: {},
+
+    // 初始化代理管理
+    init: async function() {
+        try {
+            console.log('初始化HTTP代理管理...');
+            await this.loadProxyStatus();
+            this.bindEvents();
+            return Promise.resolve();
+        } catch (error) {
+            console.error('初始化HTTP代理管理失败:', error);
+        }
+    },
+
+    // 加载代理状态
+    loadProxyStatus: async function() {
+        try {
+            const response = await fetch('/api/httpProxy/proxy/status');
+            if (!response.ok) {
+                throw new Error('获取代理状态失败');
+            }
+            
+            const status = await response.json();
+            this.updateStatusDisplay(status);
+            
+            // 加载配置
+            const configResponse = await fetch('/api/httpProxy/proxy/config');
+            if (configResponse.ok) {
+                const configData = await configResponse.json();
+                this.currentConfig = configData.config || {};
+                this.updateConfigForm();
+            }
+        } catch (error) {
+            console.error('加载代理状态失败:', error);
+            this.updateStatusDisplay({ 
+                isRunning: false, 
+                error: error.message 
+            });
+        }
+    },
+
+    // 更新状态显示
+    updateStatusDisplay: function(status) {
+        const statusElement = document.getElementById('proxyStatus');
+        const statusBadge = document.getElementById('proxyStatusBadge');
+        const portInfo = document.getElementById('proxyPortInfo');
+        
+        if (statusElement) {
+            if (status.isRunning) {
+                statusElement.textContent = '运行中';
+                statusElement.className = 'status-running';
+                if (statusBadge) {
+                    statusBadge.textContent = '运行中';
+                    statusBadge.className = 'badge badge-success';
+                }
+            } else {
+                statusElement.textContent = '已停止';
+                statusElement.className = 'status-stopped';
+                if (statusBadge) {
+                    statusBadge.textContent = '已停止';
+                    statusBadge.className = 'badge badge-secondary';
+                }
+            }
+        }
+        
+        if (portInfo && status.config) {
+            portInfo.textContent = `${status.config.host}:${status.config.port}`;
+        }
+    },
+
+    // 更新配置表单
+    updateConfigForm: function() {
+        if (!this.currentConfig) return;
+        
+        const elements = {
+            'proxy-port': this.currentConfig.port || 8080,
+            'proxy-host': this.currentConfig.host || '0.0.0.0',
+            'proxy-enable-https': this.currentConfig.enableHttps || false,
+            'proxy-enable-auth': this.currentConfig.enableAuth || false,
+            'proxy-username': this.currentConfig.username || '',
+            'proxy-password': this.currentConfig.password || '',
+            'proxy-log-requests': this.currentConfig.logRequests !== false
+        };
+        
+        for (const [id, value] of Object.entries(elements)) {
+            const element = document.getElementById(id);
+            if (element) {
+                if (element.type === 'checkbox') {
+                    element.checked = Boolean(value);
+                } else {
+                    element.value = value;
+                }
+            }
+        }
+        
+        // 更新允许和阻止的主机列表
+        this.updateHostLists();
+    },
+
+    // 更新主机列表显示
+    updateHostLists: function() {
+        const allowedList = document.getElementById('allowedHostsList');
+        const blockedList = document.getElementById('blockedHostsList');
+        
+        if (allowedList && this.currentConfig.allowedHosts) {
+            allowedList.innerHTML = this.currentConfig.allowedHosts
+                .map(host => `<span class="host-tag">${host} <button onclick="httpProxyManager.removeAllowedHost('${host}')">&times;</button></span>`)
+                .join('');
+        }
+        
+        if (blockedList && this.currentConfig.blockedHosts) {
+            blockedList.innerHTML = this.currentConfig.blockedHosts
+                .map(host => `<span class="host-tag blocked">${host} <button onclick="httpProxyManager.removeBlockedHost('${host}')">&times;</button></span>`)
+                .join('');
+        }
+    },
+
+    // 绑定事件
+    bindEvents: function() {
+        // 启动代理按钮
+        const startBtn = document.getElementById('startProxyBtn');
+        if (startBtn) {
+            startBtn.addEventListener('click', () => this.startProxy());
+        }
+        
+        // 停止代理按钮
+        const stopBtn = document.getElementById('stopProxyBtn');
+        if (stopBtn) {
+            stopBtn.addEventListener('click', () => this.stopProxy());
+        }
+        
+        // 保存配置按钮
+        const saveBtn = document.getElementById('saveProxyConfigBtn');
+        if (saveBtn) {
+            saveBtn.addEventListener('click', () => this.saveConfig());
+        }
+        
+        // 测试代理按钮
+        const testBtn = document.getElementById('testProxyBtn');
+        if (testBtn) {
+            testBtn.addEventListener('click', () => this.testProxy());
+        }
+        
+        // 添加允许主机
+        const addAllowedBtn = document.getElementById('addAllowedHostBtn');
+        if (addAllowedBtn) {
+            addAllowedBtn.addEventListener('click', () => this.addAllowedHost());
+        }
+        
+        // 添加阻止主机
+        const addBlockedBtn = document.getElementById('addBlockedHostBtn');
+        if (addBlockedBtn) {
+            addBlockedBtn.addEventListener('click', () => this.addBlockedHost());
+        }
+    },
+
+    // 启动代理
+    startProxy: async function() {
+        try {
+            const config = this.getConfigFromForm();
+            
+            const response = await fetch('/api/httpProxy/proxy/start', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify(config)
+            });
+            
+            if (!response.ok) {
+                const error = await response.json();
+                throw new Error(error.error || '启动失败');
+            }
+            
+            const result = await response.json();
+            core.showAlert('代理服务启动成功', 'success');
+            this.updateStatusDisplay(result.status);
+        } catch (error) {
+            console.error('启动代理失败:', error);
+            core.showAlert('启动代理失败: ' + error.message, 'error');
+        }
+    },
+
+    // 停止代理
+    stopProxy: async function() {
+        try {
+            const response = await fetch('/api/httpProxy/proxy/stop', {
+                method: 'POST'
+            });
+            
+            if (!response.ok) {
+                const error = await response.json();
+                throw new Error(error.error || '停止失败');
+            }
+            
+            const result = await response.json();
+            core.showAlert('代理服务已停止', 'success');
+            this.updateStatusDisplay(result.status);
+        } catch (error) {
+            console.error('停止代理失败:', error);
+            core.showAlert('停止代理失败: ' + error.message, 'error');
+        }
+    },
+
+    // 保存配置
+    saveConfig: async function() {
+        try {
+            const config = this.getConfigFromForm();
+            
+            const response = await fetch('/api/httpProxy/proxy/config', {
+                method: 'PUT',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify(config)
+            });
+            
+            if (!response.ok) {
+                const error = await response.json();
+                throw new Error(error.error || '保存配置失败');
+            }
+            
+            const result = await response.json();
+            this.currentConfig = config;
+            core.showAlert('代理配置已保存', 'success');
+            this.updateStatusDisplay(result.status);
+        } catch (error) {
+            console.error('保存配置失败:', error);
+            core.showAlert('保存配置失败: ' + error.message, 'error');
+        }
+    },
+
+    // 测试代理
+    testProxy: async function() {
+        try {
+            const testUrl = document.getElementById('proxyTestUrl')?.value || 'http://httpbin.org/ip';
+            
+            const response = await fetch('/api/httpProxy/proxy/test', {
+                method: 'POST',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({ testUrl })
+            });
+            
+            if (!response.ok) {
+                const error = await response.json();
+                throw new Error(error.error || '测试失败');
+            }
+            
+            const result = await response.json();
+            core.showAlert(`代理测试成功 (${result.responseTime})`, 'success');
+        } catch (error) {
+            console.error('代理测试失败:', error);
+            core.showAlert('代理测试失败: ' + error.message, 'error');
+        }
+    },
+
+    // 从表单获取配置
+    getConfigFromForm: function() {
+        return {
+            port: parseInt(document.getElementById('proxy-port')?.value) || 8080,
+            host: document.getElementById('proxy-host')?.value || '0.0.0.0',
+            enableHttps: document.getElementById('proxy-enable-https')?.checked || false,
+            enableAuth: document.getElementById('proxy-enable-auth')?.checked || false,
+            username: document.getElementById('proxy-username')?.value || '',
+            password: document.getElementById('proxy-password')?.value || '',
+            logRequests: document.getElementById('proxy-log-requests')?.checked !== false,
+            allowedHosts: this.currentConfig.allowedHosts || [],
+            blockedHosts: this.currentConfig.blockedHosts || []
+        };
+    },
+
+    // 添加允许的主机
+    addAllowedHost: function() {
+        const input = document.getElementById('newAllowedHost');
+        const host = input?.value?.trim();
+        
+        if (host && !this.currentConfig.allowedHosts?.includes(host)) {
+            if (!this.currentConfig.allowedHosts) {
+                this.currentConfig.allowedHosts = [];
+            }
+            this.currentConfig.allowedHosts.push(host);
+            this.updateHostLists();
+            input.value = '';
+        }
+    },
+
+    // 移除允许的主机
+    removeAllowedHost: function(host) {
+        if (this.currentConfig.allowedHosts) {
+            this.currentConfig.allowedHosts = this.currentConfig.allowedHosts.filter(h => h !== host);
+            this.updateHostLists();
+        }
+    },
+
+    // 添加阻止的主机
+    addBlockedHost: function() {
+        const input = document.getElementById('newBlockedHost');
+        const host = input?.value?.trim();
+        
+        if (host && !this.currentConfig.blockedHosts?.includes(host)) {
+            if (!this.currentConfig.blockedHosts) {
+                this.currentConfig.blockedHosts = [];
+            }
+            this.currentConfig.blockedHosts.push(host);
+            this.updateHostLists();
+            input.value = '';
+        }
+    },
+
+    // 移除阻止的主机
+    removeBlockedHost: function(host) {
+        if (this.currentConfig.blockedHosts) {
+            this.currentConfig.blockedHosts = this.currentConfig.blockedHosts.filter(h => h !== host);
+            this.updateHostLists();
+        }
+    }
+};
+
+// 全局公开模块
+window.httpProxyManager = httpProxyManager;

+ 469 - 92
hubcmdui/web/style.css

@@ -762,170 +762,547 @@
     color: white;
 }
 
+/* ===== 文档内容区域优化样式 ===== */
 #documentationText {
-    padding: 0.5rem 1.5rem; /* 增加左右内边距 */
-    max-width: 900px; /* 限制最大宽度以提高可读性 */
-    /* margin-left: auto;  移除左边距自动 */
-    /* margin-right: auto; 移除右边距自动 */
+    padding: 2rem;
+    max-width: 900px;
+    line-height: 1.7;
+    font-size: 16px;
+    color: #2d3748;
+    background: #ffffff;
+    border-radius: var(--radius-lg);
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    margin: 1rem 0;
+}
+
+/* 标题样式层次化 */
+#documentationText h1 {
+    font-size: 2.5em;
+    font-weight: 700;
+    color: #1a202c;
+    margin: 0 0 1.5rem 0;
+    padding-bottom: 0.8rem;
+    border-bottom: 3px solid var(--primary-color);
+    background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
+    -webkit-background-clip: text;
+    -webkit-text-fill-color: transparent;
+    background-clip: text;
 }
 
 #documentationText h2 {
-    font-size: 1.8em;
-    margin-top: 2.5em;
-    margin-bottom: 1em;
-    border-bottom: 1px solid #eaecef;
-    padding-bottom: 0.4em;
+    font-size: 2em;
+    font-weight: 600;
+    color: #2d3748;
+    margin: 2.5rem 0 1.2rem 0;
+    padding: 0.5rem 0 0.5rem 1rem;
+    border-left: 4px solid var(--primary-color);
+    border-bottom: 1px solid #e2e8f0;
+    background: linear-gradient(90deg, rgba(61, 124, 244, 0.05) 0%, transparent 100%);
+    border-radius: 0 8px 8px 0;
+}
+
+#documentationText h3 {
+    font-size: 1.5em;
     font-weight: 600;
+    color: #4a5568;
+    margin: 2rem 0 1rem 0;
+    padding-left: 0.5rem;
+    border-left: 3px solid var(--primary-light);
 }
 
+#documentationText h4 {
+    font-size: 1.25em;
+    font-weight: 600;
+    color: #4a5568;
+    margin: 1.5rem 0 0.8rem 0;
+}
+
+#documentationText h5 {
+    font-size: 1.1em;
+    font-weight: 600;
+    color: #4a5568;
+    margin: 1.2rem 0 0.6rem 0;
+}
+
+#documentationText h6 {
+    font-size: 1em;
+    font-weight: 600;
+    color: #718096;
+    margin: 1rem 0 0.5rem 0;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+}
+
+/* 段落和文本样式 */
 #documentationText p {
-    margin-bottom: 1.5rem; /* 增大段落间距 */
-    font-size: 1.05rem; /* 稍微增大正文字号 */
+    margin-bottom: 1.5rem;
+    font-size: 1.05rem;
+    line-height: 1.8;
+    color: #4a5568;
+    text-align: justify;
 }
 
+#documentationText p:first-of-type {
+    font-size: 1.1rem;
+    color: #2d3748;
+    font-weight: 400;
+}
+
+/* 列表样式优化 */
 #documentationText ul, #documentationText ol {
-    padding-left: 1.8em; /* 调整缩进 */
-    margin-bottom: 1.5rem;
+    padding-left: 0;
+    margin: 1.5rem 0;
+    list-style: none;
 }
 
-#documentationText li {
-    margin-bottom: 0.6rem;
+#documentationText ul li {
+    position: relative;
+    margin-bottom: 0.8rem;
+    padding-left: 2rem;
+    line-height: 1.7;
 }
 
+#documentationText ul li::before {
+    content: "▸";
+    position: absolute;
+    left: 0.5rem;
+    color: var(--primary-color);
+    font-weight: bold;
+    font-size: 1.1em;
+}
+
+#documentationText ol {
+    counter-reset: ordered-list;
+}
+
+#documentationText ol li {
+    position: relative;
+    margin-bottom: 0.8rem;
+    padding-left: 2.5rem;
+    counter-increment: ordered-list;
+    line-height: 1.7;
+}
+
+#documentationText ol li::before {
+    content: counter(ordered-list);
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 1.8rem;
+    height: 1.8rem;
+    background: var(--primary-color);
+    color: white;
+    border-radius: 50%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-weight: 600;
+    font-size: 0.9rem;
+}
+
+/* 嵌套列表样式 */
+#documentationText ul ul, #documentationText ol ul {
+    margin: 0.5rem 0;
+    padding-left: 1.5rem;
+}
+
+#documentationText ul ul li::before {
+    content: "◦";
+    color: var(--primary-light);
+}
+
+/* 代码块样式大幅优化 */
 #documentationText 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; /* 为复制按钮定位 */
+    background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
+    color: #f8fafc;
+    padding: 0;
+    border-radius: 12px;
+    overflow: hidden;
+    margin: 2rem 0;
+    box-shadow: 
+        0 10px 25px rgba(0, 0, 0, 0.2),
+        0 4px 6px rgba(0, 0, 0, 0.1),
+        inset 0 1px 0 rgba(255, 255, 255, 0.1);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    position: relative;
+    font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
 }
 
+/* 代码块头部 */
 .doc-content pre::before {
-    content: ''; /* 模拟终端窗口的顶部栏 */
+    content: attr(data-language, 'Code');
     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; /* 定位和添加下方间距 */
+    padding: 0.8rem 1.5rem;
+    background: linear-gradient(90deg, #374151 0%, #4b5563 100%);
+    color: #d1d5db;
+    font-size: 0.875rem;
+    font-weight: 600;
+    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
     position: relative;
 }
 
-/* 模拟窗口按钮 */
+/* 模拟 macOS 窗口按钮 */
 .doc-content pre::after {
     content: '';
     position: absolute;
-    top: 8px;
-    left: 15px;
+    top: 0.8rem;
+    right: 1.5rem;
     width: 12px;
     height: 12px;
-    background-color: #FF5F57;
+    background: #ff5f57;
     border-radius: 50%;
-    box-shadow: 20px 0 #FEBC2E, 40px 0 #28C840;
+    box-shadow: 
+        -20px 0 #febc2e, 
+        -40px 0 #28ca42;
 }
 
 .doc-content pre code {
-    display: block; /* 确保代码块充满 pre */
-    background-color: transparent;
-    padding: 0;
-    margin: 0;
+    display: block;
+    padding: 1.5rem;
+    background: transparent;
     color: inherit;
-    border-radius: 0;
     border: none;
-    line-height: inherit;
+    border-radius: 0;
     font-family: inherit;
-    white-space: pre; /* 保留空格和换行 */
-    font-size: inherit; /* 继承 pre 的字号 */
+    font-size: 0.95rem;
+    line-height: 1.6;
+    white-space: pre;
+    overflow-x: auto;
+    scrollbar-width: thin;
+    scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
 }
 
-/* 行内代码样式 */
+.doc-content pre code::-webkit-scrollbar {
+    height: 6px;
+}
+
+.doc-content pre code::-webkit-scrollbar-track {
+    background: transparent;
+}
+
+.doc-content pre code::-webkit-scrollbar-thumb {
+    background: rgba(255, 255, 255, 0.3);
+    border-radius: 3px;
+}
+
+.doc-content pre code::-webkit-scrollbar-thumb:hover {
+    background: rgba(255, 255, 255, 0.5);
+}
+
+/* 行内代码样式优化 */
 .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; /* 垂直对齐 */
+    font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
+    font-size: 0.9em;
+    background: linear-gradient(135deg, rgba(61, 124, 244, 0.08) 0%, rgba(61, 124, 244, 0.12) 100%);
+    color: #3d7cf4;
+    padding: 0.3rem 0.6rem;
+    margin: 0 0.2rem;
+    border-radius: 6px;
+    border: 1px solid rgba(61, 124, 244, 0.15);
+    font-weight: 500;
+    box-shadow: 0 1px 3px rgba(61, 124, 244, 0.1);
 }
 
-/* 链接样式 */
+/* 链接样式优化 */
 .doc-content a {
-    color: var(--primary-dark); /* 使用更深的蓝色 */
-    text-decoration: underline;
-    text-decoration-color: rgba(61, 124, 244, 0.4);
-    transition: all 0.2s ease;
+    color: var(--primary-color);
+    text-decoration: none;
+    font-weight: 500;
+    border-bottom: 2px solid transparent;
+    transition: all 0.3s ease;
+    position: relative;
+}
+
+.doc-content a::after {
+    content: '';
+    position: absolute;
+    bottom: -2px;
+    left: 0;
+    width: 0;
+    height: 2px;
+    background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
+    transition: width 0.3s ease;
 }
 
 .doc-content a:hover {
-    color: var(--primary-color);
-    text-decoration-color: var(--primary-color);
-    background-color: rgba(61, 124, 244, 0.05);
+    color: var(--primary-dark);
+    background: rgba(61, 124, 244, 0.05);
+    padding: 0.2rem 0.4rem;
+    margin: -0.2rem -0.4rem;
+    border-radius: 4px;
 }
 
-/* 引用块样式 */
+.doc-content a:hover::after {
+    width: 100%;
+}
+
+/* 引用块样式大幅优化 */
 .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);
+    margin: 2rem 0;
+    padding: 1.5rem 2rem;
+    background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
+    border-left: 5px solid var(--primary-color);
+    border-radius: 0 12px 12px 0;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
+    position: relative;
+    font-style: italic;
+}
+
+.doc-content blockquote::before {
+    content: '"';
+    position: absolute;
+    top: -10px;
+    left: 15px;
+    font-size: 3rem;
+    color: var(--primary-light);
+    font-family: serif;
+    line-height: 1;
+}
+
+.doc-content blockquote p {
+    margin: 0;
+    color: #4a5568;
+    font-size: 1.05rem;
+    line-height: 1.7;
 }
 
 .doc-content blockquote p:last-child {
     margin-bottom: 0;
 }
 
-/* 表格样式 */
+/* 表格样式大幅优化 */
 .doc-content table {
-    border-collapse: separate; /* 使用 separate 以应用圆角 */
+    border-collapse: separate;
     border-spacing: 0;
-    margin: 1.8rem 0;
+    margin: 2rem 0;
     width: 100%;
+    border-radius: 12px;
+    overflow: hidden;
+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
     border: 1px solid #e2e8f0;
-    border-radius: var(--radius-md);
-    overflow: hidden; /* 应用圆角 */
 }
 
-.doc-content th,
+.doc-content th {
+    background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+    color: white;
+    font-weight: 600;
+    padding: 1rem 1.5rem;
+    text-align: left;
+    font-size: 0.95rem;
+    text-transform: uppercase;
+    letter-spacing: 0.05em;
+}
+
 .doc-content td {
+    padding: 1rem 1.5rem;
     border-bottom: 1px solid #e2e8f0;
-    padding: 0.8em 1.2em;
-    text-align: left;
+    background: white;
+    color: #4a5568;
+}
+
+.doc-content tr:nth-child(even) td {
+    background: #f8fafc;
 }
 
 .doc-content tr:last-child td {
     border-bottom: none;
 }
 
-.doc-content th {
-    font-weight: 600;
-    background-color: #f7f9fc; /* 更浅的表头背景 */
-    color: #4a5568;
+.doc-content tr:hover td {
+    background: #eef2ff;
+    color: #2d3748;
 }
 
-.doc-content tr:nth-child(even) td {
-    background-color: #fafcff; /* 斑马纹 */
+/* 分隔线样式 */
+.doc-content hr {
+    border: none;
+    height: 3px;
+    background: linear-gradient(90deg, transparent, var(--primary-light), transparent);
+    margin: 3rem 0;
+    border-radius: 2px;
 }
 
-/* 元数据 (更新时间) 样式 */
+/* 图片样式优化 */
+.doc-content img {
+    max-width: 100%;
+    height: auto;
+    border-radius: 8px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    margin: 1.5rem 0;
+    transition: transform 0.3s ease;
+}
+
+.doc-content img:hover {
+    transform: scale(1.02);
+}
+
+/* 强调文本样式 */
+.doc-content strong {
+    font-weight: 700;
+    color: #2d3748;
+    background: linear-gradient(135deg, rgba(61, 124, 244, 0.1) 0%, rgba(61, 124, 244, 0.05) 100%);
+    padding: 0.1rem 0.3rem;
+    border-radius: 3px;
+}
+
+.doc-content em {
+    font-style: italic;
+    color: var(--primary-dark);
+    font-weight: 500;
+}
+
+/* 复制按钮样式优化 */
+.copy-btn {
+    position: absolute;
+    top: 0.8rem;
+    right: 4rem;
+    background: rgba(255, 255, 255, 0.1);
+    color: #d1d5db;
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    padding: 0.4rem 0.8rem;
+    border-radius: 6px;
+    font-size: 0.8rem;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    backdrop-filter: blur(10px);
+}
+
+.copy-btn:hover {
+    background: rgba(255, 255, 255, 0.2);
+    color: white;
+    transform: translateY(-1px);
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
+}
+
+.copy-btn:active {
+    transform: translateY(0);
+}
+
+/* 元数据样式优化 */
 .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; /* 确保在内容下方 */
+    margin-top: 3rem;
+    padding: 1rem 1.5rem;
+    background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
+    border-radius: 8px;
+    border-left: 4px solid var(--primary-light);
+    font-size: 0.9rem;
+    color: #718096;
+    text-align: right;
+}
+
+/* 空内容和错误状态样式优化 */
+.empty-content, .error-container {
+    text-align: center;
+    padding: 4rem 2rem;
+    color: #a0aec0;
+}
+
+.empty-content i, .error-container i {
+    font-size: 4rem;
+    margin-bottom: 1rem;
+    color: #cbd5e0;
+}
+
+.empty-content h2, .error-container h2 {
+    color: #4a5568;
+    margin-bottom: 0.5rem;
+}
+
+.empty-content p, .error-container p {
+    color: #718096;
+}
+
+/* 加载状态样式优化 */
+.loading-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 4rem 2rem;
+    color: var(--primary-color);
+}
+
+.loading-container i {
+    font-size: 2rem;
+    margin-bottom: 1rem;
+    animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+    0% { transform: rotate(0deg); }
+    100% { transform: rotate(360deg); }
+}
+
+/* 文档列表样式优化 */
+.doc-list {
+    list-style: none;
+    padding: 0;
+    margin: 0;
+}
+
+.doc-item {
+    margin-bottom: 0.5rem;
+}
+
+.doc-item a {
+    display: flex;
+    align-items: center;
+    padding: 0.75rem 1rem;
+    color: #4a5568;
+    text-decoration: none;
+    border-radius: 8px;
+    transition: all 0.3s ease;
+    border: 1px solid transparent;
+}
+
+.doc-item a:hover {
+    background: rgba(61, 124, 244, 0.05);
+    color: var(--primary-color);
+    border-color: rgba(61, 124, 244, 0.2);
+    transform: translateX(4px);
+}
+
+.doc-item a.active {
+    background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
+    color: white;
+    box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
+}
+
+.doc-item a i {
+    margin-right: 0.75rem;
+    width: 1.2rem;
+    text-align: center;
+}
+
+/* 响应式优化 */
+@media (max-width: 768px) {
+    #documentationText {
+        padding: 1.5rem;
+        margin: 0.5rem 0;
+    }
+    
+    #documentationText h1 {
+        font-size: 2rem;
+    }
+    
+    #documentationText h2 {
+        font-size: 1.75rem;
+        margin: 2rem 0 1rem 0;
+    }
+    
+    .doc-content pre {
+        margin: 1.5rem -1.5rem;
+        border-radius: 0;
+    }
+    
+    .doc-content table {
+        font-size: 0.9rem;
+    }
+    
+    .doc-content th,
+    .doc-content td {
+        padding: 0.75rem 1rem;
+    }
 }
 
 /* 加载中和消息提示 */