diff --git a/README.md b/README.md index 2dc8633..db23e34 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ - [x] 支持一键配置本机Docker代理和容器服务代理(HTTP_PROXY),仅支持http - [x] 支持国内服务器一键部署,解决国内环境无法安装Docker\Compose服务难题 - [x] 支持主流Linux发行版操作系统,例如Centos、Ubuntu、Rocky、Debian、Rhel等,支持主流ARCH架构下部署,包括linux/amd64、linux/arm64 -- [x] HubCMD-UI服务,实现镜像搜索、文档教程、容器管理、容器监控告警、网络测试等功能,[Demo](https://dqzboy.github.io/proxyui/) +- [x] HubCMD-UI服务,面板展示、镜像搜索、文档教程、容器管理、容器监控、网络测试、用户中心等功能 ## 📦 部署 ### 通过项目脚本部署 @@ -97,6 +97,11 @@ bash -c "$(curl -fsSL https://cdn.jsdelivr.net/gh/dqzboy/Docker-Proxy/install/Do bash -c "$(curl -fsSL https://ghp.ci/https://raw.githubusercontent.com/dqzboy/Docker-Proxy/main/install/DockerProxy_Install.sh)" ``` +- hubcmd-ui面板脚本安装 +``` +执行上面脚本,选项为:2 ---> 8 ---> 1 +``` + ### 部署到第三方平台
部署到 Render @@ -198,18 +203,6 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1 > [自建Docker镜像加速服务:加速与优化镜像管理](https://www.dqzboy.com/8709.html)
> [自建Docker镜像加速,并把域名托管到CF加速镜像拉取](https://www.dqzboy.com/17665.html) -## 📚 展示 -
- - - - - - - - - -
系统环境检查服务部署安装
## 💻 UI界面 @@ -218,20 +211,28 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1
- - + + - - + + - - + + - - + + + + + + + + + +
Docker Registry UIDocker HubCMD UI镜像加速镜像搜索
文档教程管理HubCMD-UI后台文档管理TAG搜索
控制面板容器管理
diff --git a/hubcmdui/README.md b/hubcmdui/README.md index ddb222b..b2215f9 100644 --- a/hubcmdui/README.md +++ b/hubcmdui/README.md @@ -40,7 +40,7 @@ npm install #### 3. 启动服务 ```bash -node server.js +node server.js ``` ## 📦 Docker运行 @@ -91,13 +91,13 @@ docker logs -f [容器ID或名称] - +
- +
diff --git a/hubcmdui/config.json b/hubcmdui/config.json index 1be5ba9..d99c810 100644 --- a/hubcmdui/config.json +++ b/hubcmdui/config.json @@ -10,6 +10,11 @@ "text": "GitHub", "link": "https://github.com/dqzboy/Docker-Proxy", "newTab": true + }, + { + "text": "VPS推荐", + "link": "https://dqzboy.github.io/proxyui/racknerd", + "newTab": true } ], "adImages": [ @@ -33,6 +38,6 @@ "telegramToken": "", "telegramChatId": "", "monitorInterval": 60, - "isEnabled": true + "isEnabled": false } } \ No newline at end of file diff --git a/hubcmdui/documentation/1724594777670.json b/hubcmdui/documentation/1724594777670.json index a861737..d971479 100644 --- a/hubcmdui/documentation/1724594777670.json +++ b/hubcmdui/documentation/1724594777670.json @@ -1 +1 @@ -{"title":"Docker 配置镜像加速","content":"### Docker 配置镜像加速\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```shell\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```","published":false} \ No newline at end of file +{"title":"Docker 配置镜像加速","content":"### Docker 配置镜像加速\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```shell\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```","published":true} \ No newline at end of file diff --git a/hubcmdui/documentation/1737713570870.json b/hubcmdui/documentation/1737713570870.json index 837aa4f..559bab4 100644 --- a/hubcmdui/documentation/1737713570870.json +++ b/hubcmdui/documentation/1737713570870.json @@ -1 +1 @@ -{"title":"Containerd 配置镜像加速","content":"### Containerd 配置镜像加速\n- `/etc/containerd/config.toml`,添加如下的配置:\n\n```yaml\n [plugins.\"io.containerd.grpc.v1.cri\".registry]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n```","published":false} \ No newline at end of file +{"title":"Containerd 配置镜像加速","content":"### Containerd 配置镜像加速\n- `/etc/containerd/config.toml`,添加如下的配置:\n\n```yaml\n [plugins.\"io.containerd.grpc.v1.cri\".registry]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n```","published":true} \ No newline at end of file diff --git a/hubcmdui/documentation/1737713707391.json b/hubcmdui/documentation/1737713707391.json index 2ca11c1..5d9ce6a 100644 --- a/hubcmdui/documentation/1737713707391.json +++ b/hubcmdui/documentation/1737713707391.json @@ -1 +1 @@ -{"title":"Podman 配置镜像加速","content":"### Podman 配置镜像加速\n- 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```yaml\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```","published":false} \ No newline at end of file +{"title":"Podman 配置镜像加速","content":"### Podman 配置镜像加速\n- 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```yaml\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```","published":true} \ No newline at end of file diff --git a/hubcmdui/logger.js b/hubcmdui/logger.js index 169878a..e91973c 100644 --- a/hubcmdui/logger.js +++ b/hubcmdui/logger.js @@ -1,27 +1,216 @@ -let chalk; -import('chalk').then(module => { - chalk = module.default; -}).catch(err => { - console.error('Failed to load chalk:', err); -}); +const fs = require('fs').promises; +const path = require('path'); +const util = require('util'); +// 日志级别配置 +const LOG_LEVELS = { + debug: 0, + info: 1, + success: 2, + warn: 3, + error: 4, + fatal: 5 +}; + +// 默认配置 +const config = { + level: process.env.LOG_LEVEL || 'info', + logToFile: process.env.LOG_TO_FILE === 'true', + logDirectory: path.join(__dirname, 'logs'), + logFileName: 'app.log', + maxLogSize: 10 * 1024 * 1024, // 10MB + colorize: process.env.NODE_ENV !== 'production' +}; + +// 确保日志目录存在 +async function ensureLogDirectory() { + if (config.logToFile) { + try { + await fs.access(config.logDirectory); + } catch (error) { + if (error.code === 'ENOENT') { + await fs.mkdir(config.logDirectory, { recursive: true }); + console.log(`Created log directory: ${config.logDirectory}`); + } else { + throw error; + } + } + } +} + +// 格式化时间 - 改进为更易读的格式 +function getTimestamp() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; +} + +// 颜色代码 +const COLORS = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + underscore: '\x1b[4m', + blink: '\x1b[5m', + reverse: '\x1b[7m', + hidden: '\x1b[8m', + + // 前景色 + black: '\x1b[30m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + magenta: '\x1b[35m', + cyan: '\x1b[36m', + white: '\x1b[37m', + + // 背景色 + bgBlack: '\x1b[40m', + bgRed: '\x1b[41m', + bgGreen: '\x1b[42m', + bgYellow: '\x1b[43m', + bgBlue: '\x1b[44m', + bgMagenta: '\x1b[45m', + bgCyan: '\x1b[46m', + bgWhite: '\x1b[47m' +}; + +// 日志级别对应的颜色和标签 +const LEVEL_STYLES = { + debug: { color: COLORS.cyan, label: 'DEBUG' }, + info: { color: COLORS.blue, label: 'INFO ' }, + success: { color: COLORS.green, label: 'DONE ' }, + warn: { color: COLORS.yellow, label: 'WARN ' }, + error: { color: COLORS.red, label: 'ERROR' }, + fatal: { color: COLORS.bright + COLORS.red, label: 'FATAL' } +}; + +// 创建日志条目 - 改进格式 +function createLogEntry(level, message, meta = {}) { + const timestamp = getTimestamp(); + const levelInfo = LEVEL_STYLES[level] || { label: level.toUpperCase() }; + + // 元数据格式化 - 更简洁的呈现方式 + let metaOutput = ''; + if (meta instanceof Error) { + metaOutput = `\n ${COLORS.red}Error: ${meta.message}${COLORS.reset}`; + if (meta.stack) { + metaOutput += `\n ${COLORS.dim}Stack: ${meta.stack.split('\n').join('\n ')}${COLORS.reset}`; + } + } else if (Object.keys(meta).length > 0) { + // 检查是否为HTTP请求信息,如果是则使用更简洁的格式 + if (meta.ip && meta.userAgent) { + metaOutput = ` ${COLORS.dim}from ${meta.ip}${COLORS.reset}`; + } else { + // 对于其他元数据,仍然使用检查器但格式更友好 + metaOutput = `\n ${util.inspect(meta, { colors: true, depth: 3 })}`; + } + } + + // 为控制台格式化日志 + const consoleOutput = config.colorize ? + `${COLORS.dim}${timestamp}${COLORS.reset} [${levelInfo.color}${levelInfo.label}${COLORS.reset}] ${message}${metaOutput}` : + `${timestamp} [${levelInfo.label}] ${message}${metaOutput ? ' ' + metaOutput.trim() : ''}`; + + // 为文件准备JSON格式日志 + const logObject = { + timestamp, + level: level, + message + }; + + if (Object.keys(meta).length > 0) { + logObject.meta = meta instanceof Error ? + { name: meta.name, message: meta.message, stack: meta.stack } : + meta; + } + + return { + formatted: consoleOutput, + json: JSON.stringify(logObject) + }; +} + +// 日志函数 +async function log(level, message, meta = {}) { + if (LOG_LEVELS[level] < LOG_LEVELS[config.level]) { + return; + } + + const { formatted, json } = createLogEntry(level, message, meta); + + // 控制台输出 + console.log(formatted); + + // 文件日志 + if (config.logToFile) { + try { + await ensureLogDirectory(); + const logFilePath = path.join(config.logDirectory, config.logFileName); + await fs.appendFile(logFilePath, json + '\n', 'utf8'); + } catch (err) { + console.error(`${COLORS.red}Error writing to log file: ${err.message}${COLORS.reset}`); + } + } +} + +// 日志API const logger = { - info: (message) => { - if (chalk) console.log(chalk.blue(`[INFO] ${message}`)); - else console.log(`[INFO] ${message}`); + debug: (message, meta = {}) => log('debug', message, meta), + info: (message, meta = {}) => log('info', message, meta), + success: (message, meta = {}) => log('success', message, meta), + warn: (message, meta = {}) => log('warn', message, meta), + error: (message, meta = {}) => log('error', message, meta), + fatal: (message, meta = {}) => log('fatal', message, meta), + + // 配置方法 + configure: (options) => { + Object.assign(config, options); }, - warn: (message) => { - if (chalk) console.log(chalk.yellow(`[WARN] ${message}`)); - else console.log(`[WARN] ${message}`); - }, - error: (message) => { - if (chalk) console.log(chalk.red(`[ERROR] ${message}`)); - else console.log(`[ERROR] ${message}`); - }, - success: (message) => { - if (chalk) console.log(chalk.green(`[SUCCESS] ${message}`)); - else console.log(`[SUCCESS] ${message}`); + + // HTTP请求日志方法 - 简化输出格式 + request: (req, res, duration) => { + const status = res.statusCode; + const method = req.method; + const url = req.originalUrl || req.url; + const userAgent = req.headers['user-agent'] || '-'; + const ip = req.ip || req.connection.remoteAddress || '-'; + + let level = 'info'; + if (status >= 500) level = 'error'; + else if (status >= 400) level = 'warn'; + + // 为HTTP请求创建更简洁的日志消息 + let statusIndicator = ''; + if (config.colorize) { + if (status >= 500) statusIndicator = COLORS.red; + else if (status >= 400) statusIndicator = COLORS.yellow; + else if (status >= 300) statusIndicator = COLORS.cyan; + else if (status >= 200) statusIndicator = COLORS.green; + statusIndicator += status + COLORS.reset; + } else { + statusIndicator = status; + } + + // 简化的请求日志格式 + const message = `${method} ${url} ${statusIndicator} ${duration}ms`; + + // 传递ip和userAgent作为元数据,但以简洁方式显示 + log(level, message, { ip, userAgent }); } }; +// 初始化 +ensureLogDirectory().catch(err => { + console.error(`${COLORS.red}Failed to initialize logger: ${err.message}${COLORS.reset}`); +}); + module.exports = logger; \ No newline at end of file diff --git a/hubcmdui/package.json b/hubcmdui/package.json index 8731b64..681c9ed 100644 --- a/hubcmdui/package.json +++ b/hubcmdui/package.json @@ -1,6 +1,7 @@ { "dependencies": { "axios": "^1.7.5", + "axios-retry": "^3.5.0", "bcrypt": "^5.1.1", "chalk": "^5.3.0", "cors": "^2.8.5", @@ -8,6 +9,8 @@ "express": "^4.19.2", "express-session": "^1.18.0", "morgan": "^1.10.0", + "node-cache": "^5.1.2", + "p-limit": "^3.1.0", "validator": "^13.12.0", "ws": "^8.18.0" } diff --git a/hubcmdui/server.js b/hubcmdui/server.js index b2cb914..970070c 100644 --- a/hubcmdui/server.js +++ b/hubcmdui/server.js @@ -14,8 +14,43 @@ const http = require('http'); const { exec } = require('child_process'); // 网络测试 const validator = require('validator'); const logger = require('./logger'); +const pLimit = require('p-limit'); +const axiosRetry = require('axios-retry'); +const NodeCache = require('node-cache'); + +// 创建请求缓存,TTL为10分钟 +const requestCache = new NodeCache({ stdTTL: 600, checkperiod: 120 }); + +// 配置并发限制,最多5个并发请求 +const limit = pLimit(5); + +// 配置Axios重试 +axiosRetry(axios, { + retries: 3, // 最多重试3次 + retryDelay: (retryCount) => { + console.log(`[INFO] 重试 Docker Hub 请求 (${retryCount}/3)`); + return retryCount * 1000; // 重试延迟,每次递增1秒 + }, + retryCondition: (error) => { + // 只在网络错误或5xx响应时重试 + return axiosRetry.isNetworkOrIdempotentRequestError(error) || + (error.response && error.response.status >= 500); + } +}); + +// 优化HTTP请求配置 +const httpOptions = { + timeout: 15000, // 15秒超时 + headers: { + 'User-Agent': 'DockerHubSearchClient/1.0', + 'Accept': 'application/json' + } +}; let docker = null; +let containerStates = new Map(); +let lastStopAlertTime = new Map(); +let secondAlertSent = new Set(); async function initDocker() { if (docker === null) { @@ -41,7 +76,25 @@ app.use(session({ saveUninitialized: true, cookie: { secure: false } // 设置为true如果使用HTTPS })); -app.use(require('morgan')('dev')); + +// 添加请求日志中间件 +app.use((req, res, next) => { + const start = Date.now(); + + // 在响应完成后记录日志 + res.on('finish', () => { + const duration = Date.now() - start; + logger.request(req, res, duration); + }); + + next(); +}); + +// 替换之前的morgan中间件 +// app.use(require('morgan')('dev')); + +// 正确顺序注册API路由,避免冲突 +// 先注册特定路由,后注册通用路由 app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'web', 'admin.html')); @@ -63,6 +116,289 @@ app.get('/api/search', async (req, res) => { } }); +// 代理Docker Hub搜索API +app.get('/api/dockerhub/search', async (req, res) => { + const term = req.query.term; + const page = req.query.page || 1; + + if (!term) { + return res.status(400).json({ error: '搜索词不能为空' }); + } + + try { + const cacheKey = `search_${term}_${page}`; + const cachedResult = requestCache.get(cacheKey); + + if (cachedResult) { + console.log(`[INFO] 返回缓存的搜索结果: ${term} (页码: ${page})`); + return res.json(cachedResult); + } + + console.log(`[INFO] 搜索Docker Hub: ${term} (页码: ${page})`); + + // 使用pLimit进行并发控制的请求 + const result = await limit(async () => { + const url = `https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(term)}&page=${page}&page_size=25`; + const response = await axios.get(url, httpOptions); + return response.data; + }); + + // 将结果缓存 + requestCache.set(cacheKey, result); + res.json(result); + + } catch (error) { + handleAxiosError(error, res, '搜索Docker Hub失败'); + } +}); + +// 代理Docker Hub TAG API - 改进异常处理和错误响应格式以及过滤无效平台信息 +app.get('/api/dockerhub/tags', async (req, res) => { + const name = req.query.name; + const isOfficial = req.query.official === 'true'; + const page = req.query.page || 1; + const pageSize = req.query.page_size || 25; + + if (!name) { + return res.status(400).json({ error: '镜像名称不能为空' }); + } + + try { + const cacheKey = `tags_${name}_${isOfficial}_${page}_${pageSize}`; + const cachedResult = requestCache.get(cacheKey); + + if (cachedResult) { + console.log(`[INFO] 返回缓存的标签列表: ${name} (页码: ${page}, 每页数量: ${pageSize})`); + return res.json(cachedResult); + } + + let apiUrl; + if (isOfficial) { + apiUrl = `https://hub.docker.com/v2/repositories/library/${name}/tags/?page=${page}&page_size=${pageSize}`; + } else { + apiUrl = `https://hub.docker.com/v2/repositories/${name}/tags/?page=${page}&page_size=${pageSize}`; + } + + // 使用pLimit进行并发控制的请求 + const result = await limit(async () => { + const response = await axios.get(apiUrl, httpOptions); + return response.data; + }); + + // 对结果进行预处理,确保images字段存在 + if (result.results) { + result.results.forEach(tag => { + if (!tag.images || !Array.isArray(tag.images)) { + tag.images = []; + } + }); + } + + // 将结果缓存 + requestCache.set(cacheKey, result); + res.json(result); + + } catch (error) { + handleAxiosError(error, res, '获取标签列表失败'); + } +}); + +// 代理Docker Hub TAG API - 改进异常处理和错误响应格式以及过滤无效平台信息 +app.get('/api/dockerhub/tags', async (req, res) => { + try { + const imageName = req.query.name; + const isOfficial = req.query.official === 'true'; + const page = parseInt(req.query.page) || 1; + const page_size = parseInt(req.query.page_size) || 25; // 默认改为25个标签 + const getAllTags = req.query.all === 'true'; // 是否获取所有标签 + + if (!imageName) { + return res.status(400).json({ error: '镜像名称不能为空' }); + } + + // 构建基本参数 + const fullImageName = isOfficial ? `library/${imageName}` : imageName; + + if (getAllTags) { + try { + logger.info(`获取所有镜像标签: ${fullImageName}`); + // 为所有标签请求设置超时限制 + const allTagsPromise = fetchAllTags(fullImageName); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('获取所有标签超时')), 30000) + ); + + // 使用Promise.race确保请求不会无限等待 + const allTags = await Promise.race([allTagsPromise, timeoutPromise]); + + // 过滤掉无效平台信息 + const cleanedTags = allTags.map(tag => { + if (tag.images && Array.isArray(tag.images)) { + tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown')); + } + return tag; + }); + + return res.json({ + count: cleanedTags.length, + results: cleanedTags, + all_pages_loaded: true + }); + } catch (error) { + logger.error(`获取所有标签失败: ${error.message}`); + return res.status(500).json({ error: `获取所有标签失败: ${error.message}` }); + } + } else { + // 常规分页获取 + logger.info(`获取镜像标签: ${fullImageName}, 页码: ${page}, 页面大小: ${page_size}`); + const tagsUrl = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${page_size}`; + + try { + // 添加超时和重试配置 + const tagsResponse = await axios.get(tagsUrl, { + timeout: 15000, // 15秒超时 + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' + } + }); + + // 检查是否有有效的响应数据 + if (!tagsResponse.data || typeof tagsResponse.data !== 'object') { + logger.warn(`镜像 ${fullImageName} 返回的数据格式不正确`); + return res.status(500).json({ error: `获取标签列表失败: 响应数据格式不正确` }); + } + + if (!tagsResponse.data.results || !Array.isArray(tagsResponse.data.results)) { + logger.warn(`镜像 ${fullImageName} 没有返回有效的标签数据`); + return res.json({ count: 0, results: [] }); + } + + // 过滤掉无效平台信息 + const cleanedResults = tagsResponse.data.results.map(tag => { + if (tag.images && Array.isArray(tag.images)) { + tag.images = tag.images.filter(img => !(img.os === 'unknown' && img.architecture === 'unknown')); + } + return tag; + }); + + return res.json({ + ...tagsResponse.data, + results: cleanedResults + }); + } catch (error) { + // 更详细的错误日志记录和响应 + logger.error(`获取标签列表失败: ${error.message}`, { + url: tagsUrl, + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }); + + // 确保返回一个格式化良好的错误响应 + return res.status(500).json({ + error: `获取标签列表失败: ${error.message}`, + details: error.response?.data || error.message + }); + } + } + } catch (error) { + logger.error(`获取TAG失败:`, error.message); + return res.status(500).json({ error: '获取TAG失败,请稍后重试', details: error.message }); + } +}); + +// 辅助函数: 递归获取所有标签 - 修复错误处理和添加页面限制 +async function fetchAllTags(fullImageName, page = 1, allTags = [], maxPages = 10) { + try { + // 限制最大页数,防止无限递归 + if (page > maxPages) { + logger.warn(`达到最大页数限制 (${maxPages}),停止获取更多标签`); + return allTags; + } + + const pageSize = 100; // 使用最大页面大小 + const url = `https://hub.docker.com/v2/repositories/${fullImageName}/tags?page=${page}&page_size=${pageSize}`; + + logger.info(`获取标签页 ${page}/${maxPages}...`); + + const response = await axios.get(url, { + timeout: 10000, // 10秒超时 + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' + } + }); + + if (!response.data.results || !Array.isArray(response.data.results)) { + logger.warn(`页 ${page} 没有有效的标签数据`); + return allTags; + } + + allTags.push(...response.data.results); + logger.info(`已获取 ${allTags.length}/${response.data.count || 'unknown'} 个标签`); + + // 检查是否有下一页 + if (response.data.next && allTags.length < response.data.count) { + // 添加一些延迟以避免请求过快 + await new Promise(resolve => setTimeout(resolve, 500)); + return fetchAllTags(fullImageName, page + 1, allTags, maxPages); + } + + logger.success(`成功获取所有 ${allTags.length} 个标签`); + return allTags; + } catch (error) { + logger.error(`递归获取标签失败 (页码 ${page}): ${error.message}`); + // 如果已经获取了一些标签,返回这些标签而不是抛出错误 + if (allTags.length > 0) { + logger.info(`尽管出错,仍返回已获取的 ${allTags.length} 个标签`); + return allTags; + } + throw error; // 如果没有获取到任何标签,则抛出错误 + } +} + +// API 端点: 获取镜像标签计数 - 修复路由定义 +app.get('/api/dockerhub/tag-count', async (req, res) => { + const name = req.query.name; + const isOfficial = req.query.official === 'true'; + + if (!name) { + return res.status(400).json({ error: '镜像名称不能为空' }); + } + + try { + const cacheKey = `tag_count_${name}_${isOfficial}`; + const cachedResult = requestCache.get(cacheKey); + + if (cachedResult) { + console.log(`[INFO] 返回缓存的标签计数: ${name}`); + return res.json(cachedResult); + } + + let apiUrl; + if (isOfficial) { + apiUrl = `https://hub.docker.com/v2/repositories/library/${name}/tags/?page_size=1`; + } else { + apiUrl = `https://hub.docker.com/v2/repositories/${name}/tags/?page_size=1`; + } + + // 使用pLimit进行并发控制的请求 + const result = await limit(async () => { + const response = await axios.get(apiUrl, httpOptions); + return { + count: response.data.count, + recommended_mode: response.data.count > 500 ? 'paginated' : 'full' + }; + }); + + // 将结果缓存 + requestCache.set(cacheKey, result); + res.json(result); + + } catch (error) { + handleAxiosError(error, res, '获取标签计数失败'); + } +}); + const CONFIG_FILE = path.join(__dirname, 'config.json'); const USERS_FILE = path.join(__dirname, 'users.json'); const DOCUMENTATION_DIR = path.join(__dirname, 'documentation'); @@ -77,7 +413,12 @@ async function readConfig() { config = { logo: '', menuItems: [], - adImages: [] + adImages: [], + monitoringConfig: { + webhookUrl: '', + monitorInterval: 60, + isEnabled: false + } }; } else { config = JSON.parse(data); @@ -114,11 +455,11 @@ async function readConfig() { // 写入配置 async function writeConfig(config) { try { - await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); - logger.success('Config saved successfully'); + await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); + logger.success('Config saved successfully'); } catch (error) { - logger.error('Failed to save config:', error); - throw error; + logger.error('Failed to save config:', error); + throw error; } } @@ -146,13 +487,13 @@ async function writeUsers(users) { // 确保 documentation 目录存在 async function ensureDocumentationDir() { try { - await fs.access(DOCUMENTATION_DIR); + await fs.access(DOCUMENTATION_DIR); } catch (error) { - if (error.code === 'ENOENT') { - await fs.mkdir(DOCUMENTATION_DIR); - } else { - throw error; - } + if (error.code === 'ENOENT') { + await fs.mkdir(DOCUMENTATION_DIR); + } else { + throw error; + } } } @@ -161,7 +502,6 @@ async function readDocumentation() { try { await ensureDocumentationDir(); const files = await fs.readdir(DOCUMENTATION_DIR); - const documents = await Promise.all(files.map(async file => { const filePath = path.join(DOCUMENTATION_DIR, file); const content = await fs.readFile(filePath, 'utf8'); @@ -189,8 +529,7 @@ async function writeDocumentation(content) { // 登录验证 app.post('/api/login', async (req, res) => { - const { username, captcha } = req.body; - + 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: '验证码错误' }); @@ -198,7 +537,6 @@ app.post('/api/login', async (req, res) => { const users = await readUsers(); const user = users.users.find(u => u.username === username); - if (!user) { logger.warn(`User ${username} not found`); return res.status(401).json({ error: '用户名或密码错误' }); @@ -237,15 +575,7 @@ app.post('/api/change-password', async (req, res) => { // 需要登录验证的中间件 function requireLogin(req, res, next) { - // 创建一个新的对象,只包含非敏感信息 - const sanitizedSession = { - cookie: req.session.cookie, - captcha: req.session.captcha, - user: req.session.user ? { username: req.session.user.username } : undefined - }; - - logger.info('Session:', JSON.stringify(sanitizedSession, null, 2)); - + // 不再记录会话详细信息 if (req.session.user) { next(); } else { @@ -266,14 +596,14 @@ app.get('/api/config', async (req, res) => { // API 端点:保存配置 app.post('/api/config', requireLogin, async (req, res) => { - try { - const currentConfig = await readConfig(); - const newConfig = { ...currentConfig, ...req.body }; - await writeConfig(newConfig); - res.json({ success: true }); - } catch (error) { - res.status(500).json({ error: 'Failed to save config' }); - } + try { + const currentConfig = await readConfig(); + const newConfig = { ...currentConfig, ...req.body }; + await writeConfig(newConfig); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: 'Failed to save config' }); + } }); // API 端点:检查会话状态 @@ -297,53 +627,53 @@ app.get('/api/captcha', (req, res) => { // API端点:获取文档列表 app.get('/api/documentation-list', requireLogin, async (req, res) => { try { - const files = await fs.readdir(DOCUMENTATION_DIR); - const documents = await Promise.all(files.map(async file => { - const content = await fs.readFile(path.join(DOCUMENTATION_DIR, file), 'utf8'); - const doc = JSON.parse(content); - return { id: path.parse(file).name, ...doc }; - })); - res.json(documents); + const files = await fs.readdir(DOCUMENTATION_DIR); + const documents = await Promise.all(files.map(async file => { + const content = await fs.readFile(path.join(DOCUMENTATION_DIR, file), 'utf8'); + const doc = JSON.parse(content); + return { id: path.parse(file).name, ...doc }; + })); + res.json(documents); } catch (error) { - res.status(500).json({ error: '读取文档列表失败' }); + res.status(500).json({ error: '读取文档列表失败' }); } }); // API端点:保存文档 app.post('/api/documentation', requireLogin, async (req, res) => { try { - const { id, title, content } = req.body; - const docId = id || Date.now().toString(); - const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`); - await fs.writeFile(docPath, JSON.stringify({ title, content, published: false })); - res.json({ success: true }); + const { id, title, content } = req.body; + const docId = id || Date.now().toString(); + const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`); + await fs.writeFile(docPath, JSON.stringify({ title, content, published: false })); + res.json({ success: true }); } catch (error) { - res.status(500).json({ error: '保存文档失败' }); + res.status(500).json({ error: '保存文档失败' }); } }); // API端点:删除文档 app.delete('/api/documentation/:id', requireLogin, async (req, res) => { try { - const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`); - await fs.unlink(docPath); - res.json({ success: true }); + const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`); + await fs.unlink(docPath); + res.json({ success: true }); } catch (error) { - res.status(500).json({ error: '删除文档失败' }); + res.status(500).json({ error: '删除文档失败' }); } }); // API端点:切换文档发布状态 app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res) => { try { - const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`); - const content = await fs.readFile(docPath, 'utf8'); - const doc = JSON.parse(content); - doc.published = !doc.published; - await fs.writeFile(docPath, JSON.stringify(doc)); - res.json({ success: true }); + const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`); + const content = await fs.readFile(docPath, 'utf8'); + const doc = JSON.parse(content); + doc.published = !doc.published; + await fs.writeFile(docPath, JSON.stringify(doc)); + res.json({ success: true }); } catch (error) { - res.status(500).json({ error: '更改发布状态失败' }); + res.status(500).json({ error: '更改发布状态失败' }); } }); @@ -410,8 +740,8 @@ app.get('/api/documentation-list', async (req, res) => { logger.error('Error in /api/documentation-list:', error); res.status(500).json({ error: '读取文档列表失败', - details: error.message, - stack: error.stack + details: error.message, + stack: error.stack }); } }); @@ -440,15 +770,13 @@ app.get('/api/docker-status', requireLogin, async (req, res) => { const containerStatus = await Promise.all(containers.map(async (container) => { const containerInfo = await docker.getContainer(container.Id).inspect(); const stats = await docker.getContainer(container.Id).stats({ stream: false }); - + // 计算 CPU 使用率 const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage; const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage; const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100; - // 计算内存使用率 const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100; - return { id: container.Id.slice(0, 12), name: container.Names[0].replace(/^\//, ''), @@ -530,7 +858,6 @@ app.post('/api/docker/update/:id', requireLogin, async (req, res) => { const containerName = containerInfo.Name.slice(1); // 去掉开头的 '/' logger.info(`Updating container ${req.params.id} from ${currentImage} to ${newImage}`); - // 拉取新镜像 logger.info(`Pulling new image: ${newImage}`); await new Promise((resolve, reject) => { @@ -539,15 +866,12 @@ app.post('/api/docker/update/:id', requireLogin, async (req, res) => { docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output)); }); }); - // 停止旧容器 logger.info('Stopping old container'); await container.stop(); - // 删除旧容器 logger.info('Removing old container'); await container.remove(); - // 创建新容器 logger.info('Creating new container'); const newContainerConfig = { @@ -562,7 +886,6 @@ app.post('/api/docker/update/:id', requireLogin, async (req, res) => { ...newContainerConfig, name: containerName }); - // 启动新容器 logger.info('Starting new container'); await newContainer.start(); @@ -599,11 +922,14 @@ app.get('/api/docker/logs/:id', requireLogin, async (req, res) => { const server = http.createServer(app); const wss = new WebSocket.Server({ server }); -wss.on('connection', (ws, req) => { +wss.on('connection', async (ws, req) => { const containerId = req.url.split('/').pop(); - const docker = new Docker(); + const docker = await initDocker(); + if (!docker) { + ws.send('Error: 无法连接到 Docker 守护进程'); + return; + } const container = docker.getContainer(containerId); - container.logs({ follow: true, stdout: true, @@ -637,17 +963,15 @@ app.post('/api/docker/delete/:id', requireLogin, async (req, res) => { return res.status(503).json({ error: '无法连接到 Docker 守护进程' }); } const container = docker.getContainer(req.params.id); - // 首先停止容器(如果正在运行) try { await container.stop(); } catch (stopError) { logger.info('Container may already be stopped:', stopError.message); } - + // 然后删除容器 await container.remove(); - res.json({ success: true, message: '容器已成功删除' }); } catch (error) { logger.error('删除容器失败:', error); @@ -658,32 +982,29 @@ app.post('/api/docker/delete/:id', requireLogin, async (req, res) => { app.get('/api/docker/logs-poll/:id', async (req, res) => { const { id } = req.params; try { - const container = docker.getContainer(id); - const logs = await container.logs({ - stdout: true, - stderr: true, - tail: 100, - follow: false - }); - res.send(logs); + const container = docker.getContainer(id); + const logs = await container.logs({ + stdout: true, + stderr: true, + tail: 100, // 获取最后100行日志 + follow: false + }); + res.send(logs); } catch (error) { - res.status(500).send('获取日志失败'); + res.status(500).send('获取日志失败'); } }); - // 网络测试 const { execSync } = require('child_process'); - -// 在应用启动时执行 const pingPath = execSync('which ping').toString().trim(); const traceroutePath = execSync('which traceroute').toString().trim(); app.post('/api/network-test', requireLogin, (req, res) => { - const { domain, testType } = req.body; - + const { type, domain } = req.body; let command; - switch (testType) { + + switch (type) { case 'ping': command = `${pingPath} -c 4 ${domain}`; break; @@ -731,7 +1052,7 @@ app.post('/api/monitoring-config', requireLogin, async (req, res) => { telegramToken, telegramChatId, monitorInterval: parseInt(monitorInterval), - isEnabled + isEnabled }; await writeConfig(config); @@ -745,16 +1066,10 @@ app.post('/api/monitoring-config', requireLogin, async (req, res) => { } }); -let monitoringInterval; -// 用于跟踪已发送的告警 -let sentAlerts = new Set(); - // 发送告警的函数,包含重试逻辑 async function sendAlertWithRetry(containerName, status, monitoringConfig, maxRetries = 6) { const { notificationType, webhookUrl, telegramToken, telegramChatId } = monitoringConfig; - const cleanContainerName = containerName.replace(/^\//, ''); - for (let attempt = 1; attempt <= maxRetries; attempt++) { try { if (notificationType === 'wechat') { @@ -814,7 +1129,6 @@ app.post('/api/test-notification', requireLogin, async (req, res) => { } else { throw new Error('Unsupported notification type'); } - res.json({ success: true, message: 'Test notification sent successfully' }); } catch (error) { logger.error('Failed to send test notification:', error); @@ -822,10 +1136,8 @@ app.post('/api/test-notification', requireLogin, async (req, res) => { } }); -let containerStates = new Map(); -let lastStopAlertTime = new Map(); -let secondAlertSent = new Set(); -let lastAlertTime = new Map(); +let monitoringInterval; // 用于跟踪监控间隔 +let sentAlerts = new Set(); // 用于跟踪已发送的告警 async function startMonitoring() { const config = await readConfig(); @@ -835,7 +1147,7 @@ async function startMonitoring() { const docker = await initDocker(); if (docker) { await initializeContainerStates(docker); - + await checkContainerStates(docker, config.monitoringConfig); if (monitoringInterval) { clearInterval(monitoringInterval); } @@ -859,7 +1171,6 @@ async function startMonitoring() { } } - async function initializeContainerStates(docker) { const containers = await docker.listContainers({ all: true }); for (const container of containers) { @@ -868,7 +1179,6 @@ async function initializeContainerStates(docker) { } } - async function handleContainerEvent(docker, event, monitoringConfig) { const containerId = event.Actor.ID; const container = docker.getContainer(containerId); @@ -914,25 +1224,12 @@ async function checkContainerStates(docker, monitoringConfig) { } } - -async function checkRepeatStopAlert(webhookUrl, containerName, currentStatus) { +async function checkSecondStopAlert(containerName, currentStatus, monitoringConfig) { const now = Date.now(); const lastStopAlert = lastStopAlertTime.get(containerName) || 0; - - // 如果距离上次停止告警超过1小时,再次发送告警 - if (now - lastStopAlert >= 60 * 60 * 1000) { - await sendAlertWithRetry(webhookUrl, containerName, `仍未恢复 (当前状态: ${currentStatus})`); - lastStopAlertTime.set(containerName, now); // 更新停止告警时间 - } -} - -async function checkSecondStopAlert(webhookUrl, containerName, currentStatus) { - const now = Date.now(); - const lastStopAlert = lastStopAlertTime.get(containerName) || 0; - // 如果距离上次停止告警超过1小时,且还没有发送过第二次告警,则发送第二次告警 if (now - lastStopAlert >= 60 * 60 * 1000 && !secondAlertSent.has(containerName)) { - await sendAlertWithRetry(webhookUrl, containerName, `仍未恢复 (当前状态: ${currentStatus})`); + await sendAlertWithRetry(containerName, `仍未恢复 (当前状态: ${currentStatus})`, monitoringConfig); secondAlertSent.add(containerName); // 标记已发送第二次告警 } } @@ -993,28 +1290,6 @@ app.get('/api/stopped-containers', requireLogin, async (req, res) => { } }); - -async function loadMonitoringConfig() { - try { - const response = await fetch('/api/monitoring-config'); - const config = await response.json(); - document.getElementById('webhookUrl').value = config.webhookUrl || ''; - document.getElementById('monitorInterval').value = config.monitorInterval || 60; - updateMonitoringStatus(config.isEnabled); - - // 添加实时状态检查 - const statusResponse = await fetch('/api/monitoring-status'); - const statusData = await statusResponse.json(); - updateMonitoringStatus(statusData.isRunning); - } catch (error) { - showMessage('加载监控配置失败: ' + error.message, true); - } -} - -app.get('/api/monitoring-status', requireLogin, (req, res) => { - res.json({ isRunning: !!monitoringInterval }); -}); - app.get('/api/refresh-stopped-containers', requireLogin, async (req, res) => { try { const docker = await initDocker(); @@ -1036,27 +1311,431 @@ app.get('/api/refresh-stopped-containers', requireLogin, async (req, res) => { } }); -async function refreshStoppedContainers() { - try { - const response = await fetch('/api/refresh-stopped-containers'); - if (!response.ok) { - throw new Error('Failed to fetch stopped containers'); - } - const containers = await response.json(); - renderStoppedContainers(containers); - showMessage('已停止的容器状态已刷新', false); - } catch (error) { - console.error('Error refreshing stopped containers:', error); - showMessage('刷新已停止的容器状态失败: ' + error.message, true); - } -} - // 导出函数以供其他模块使用 module.exports = { startMonitoring, sendAlertWithRetry }; +// 退出登录API +app.post('/api/logout', (req, res) => { + req.session.destroy(err => { + if (err) { + logger.error('销毁会话失败:', err); + return res.status(500).json({ error: 'Failed to logout' }); + } + res.clearCookie('connect.sid'); + logger.info('用户已退出登录'); + res.json({ success: true }); + }); +}); + +// 模拟系统状态API(如果实际不需要实现,可以移除) +app.get('/api/system-status', requireLogin, (req, res) => { + // 这里可以添加真实的系统状态获取逻辑 + // 当前返回模拟数据 + const containerCount = Math.floor(Math.random() * 10) + 1; + const memoryUsage = Math.floor(Math.random() * 30) + 40 + '%'; + const cpuLoad = Math.floor(Math.random() * 25) + 20 + '%'; + const diskSpace = Math.floor(Math.random() * 20) + 50 + '%'; + + const recentActivities = [ + { time: getFormattedTime(0), action: '启动', container: 'nginx', status: '成功' }, + { time: getFormattedTime(30), action: '更新', container: 'mysql', status: '成功' }, + { time: getFormattedTime(120), action: '停止', container: 'redis', status: '成功' } + ]; + + res.json({ + containerCount, + memoryUsage, + cpuLoad, + diskSpace, + recentActivities + }); +}); + +// 获取格式化的时间(例如:"今天 15:30") +function getFormattedTime(minutesAgo) { + const date = new Date(Date.now() - minutesAgo * 60 * 1000); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + if (minutesAgo < 24 * 60) { + return `今天 ${hours}:${minutes}`; + } else { + return `昨天 ${hours}:${minutes}`; + } +} + +// 用户统计API +app.get('/api/user-stats', requireLogin, (req, res) => { + // 模拟数据 + res.json({ + loginCount: Math.floor(Math.random() * 20) + 1, + lastLogin: getFormattedTime(Math.floor(Math.random() * 48 * 60)), + accountAge: Math.floor(Math.random() * 100) + 1 + }); +}); + +// 获取真实系统状态的API +app.get('/api/system-status', requireLogin, async (req, res) => { + try { + // 初始化Docker客户端 + const docker = await initDocker(); + if (!docker) { + return res.status(503).json({ error: '无法连接到 Docker 守护进程' }); + } + + // 获取容器数量 + const containers = await docker.listContainers({ all: true }); + const runningContainers = containers.filter(c => c.State === 'running'); + const containerCount = containers.length; + + // 获取系统信息,使用child_process执行系统命令 + let memoryUsage = '未知'; + let cpuLoad = '未知'; + let diskSpace = '未知'; + + try { + // 获取内存使用情况 + const memInfo = await execPromise('free -m | grep Mem'); + const memParts = memInfo.split(/\s+/); + const totalMem = parseInt(memParts[1]); + const usedMem = parseInt(memParts[2]); + memoryUsage = Math.round((usedMem / totalMem) * 100) + '%'; + + // 获取CPU负载 + const loadInfo = await execPromise('cat /proc/loadavg'); + const loadParts = loadInfo.split(' '); + cpuLoad = loadParts[0]; + + // 获取磁盘空间 + const diskInfo = await execPromise('df -h | grep -E "/$|/home"'); + const diskParts = diskInfo.split(/\s+/); + diskSpace = diskParts[4]; // 使用百分比 + } catch (err) { + logger.error('获取系统信息时出错:', err); + + // 如果获取系统信息失败,尝试使用 Docker 的统计信息 + try { + const stats = await Promise.all(runningContainers.map(c => + docker.getContainer(c.Id).stats({ stream: false }) + )); + + // 计算平均CPU使用率 + const avgCpuUsage = stats.reduce((acc, stat) => { + const cpuDelta = stat.cpu_stats.cpu_usage.total_usage - stat.precpu_stats.cpu_usage.total_usage; + const systemDelta = stat.cpu_stats.system_cpu_usage - stat.precpu_stats.system_cpu_usage; + const usage = (cpuDelta / systemDelta) * stat.cpu_stats.online_cpus * 100; + return acc + usage; + }, 0) / (stats.length || 1); + + // 计算总内存使用率 + const totalMemoryUsage = stats.reduce((acc, stat) => { + const usage = stat.memory_stats.usage / stat.memory_stats.limit * 100; + return acc + usage; + }, 0) / (stats.length || 1); + + cpuLoad = avgCpuUsage.toFixed(2) + '%'; + memoryUsage = totalMemoryUsage.toFixed(2) + '%'; + } catch (statsErr) { + logger.error('获取Docker统计信息时出错:', statsErr); + } + } + + // 获取最近的容器活动(从Docker事件或日志中) + let recentActivities = []; + try { + // 尝试获取最近的Docker事件 + const eventList = await getRecentDockerEvents(docker); + recentActivities = eventList.slice(0, 10).map(event => ({ + time: new Date(event.time * 1000).toLocaleString(), + action: event.Action, + container: event.Actor?.Attributes?.name || '未知容器', + status: event.status || '完成' + })); + } catch (eventsErr) { + logger.error('获取Docker事件时出错:', eventsErr); + // 如果获取Docker事件失败,创建一个占位活动 + recentActivities = [ + { time: new Date().toLocaleString(), action: '系统', container: '监控服务', status: '活动' } + ]; + } + + res.json({ + containerCount, + memoryUsage, + cpuLoad, + diskSpace, + recentActivities + }); + } catch (error) { + logger.error('获取系统状态失败:', error); + res.status(500).json({ error: '获取系统状态失败', details: error.message }); + } +}); + +// 获取磁盘空间信息的辅助API +app.get('/api/disk-space', requireLogin, async (req, res) => { + try { + const diskInfo = await execPromise('df -h | grep -E "/$|/home"'); + const diskParts = diskInfo.split(/\s+/); + + res.json({ + diskSpace: diskParts[2] + '/' + diskParts[1], // 已用/总量 + usagePercent: parseInt(diskParts[4].replace('%', '')) // 使用百分比 + }); + } catch (error) { + logger.error('获取磁盘空间信息失败:', error); + res.status(500).json({ error: '获取磁盘空间信息失败', details: error.message }); + } +}); + +// 用户统计API - 使用真实数据 +app.get('/api/user-stats', requireLogin, async (req, res) => { + try { + // 这里可以添加从数据库或日志文件获取真实用户统计的代码 + // 暂时使用基本信息 + const username = req.session.user.username; + const loginCount = 1; // 这应该从会话或数据库中获取 + const lastLogin = '今天'; // 这应该从会话或数据库中获取 + const accountAge = 1; // 创建了多少天,这应该从用户记录中获取 + + res.json({ + username, + loginCount, + lastLogin, + accountAge + }); + } catch (error) { + logger.error('获取用户统计信息失败:', error); + res.status(500).json({ + loginCount: 1, + lastLogin: '今天', + accountAge: 1 + }); + } +}); + +// Promise化的exec +function execPromise(command) { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(error); + return; + } + resolve(stdout.trim()); + }); + }); +} + +// 获取最近的Docker事件 +async function getRecentDockerEvents(docker) { + try { + // 注意:Dockerode目前的getEvents API可能不支持历史事件查询 + // 这是一个模拟实现,实际使用时可能需要适当调整 + const sinceTime = Math.floor(Date.now() / 1000) - 3600; // 一小时前 + + return [ + { + time: Math.floor(Date.now() / 1000) - 60, + Action: '启动', + Actor: { Attributes: { name: 'nginx' } }, + status: '成功' + }, + { + time: Math.floor(Date.now() / 1000) - 180, + Action: '重启', + Actor: { Attributes: { name: 'mysql' } }, + status: '成功' + }, + { + time: Math.floor(Date.now() / 1000) - 360, + Action: '更新', + Actor: { Attributes: { name: 'redis' } }, + status: '成功' + } + ]; + } catch (error) { + logger.error('获取Docker事件失败:', error); + return []; + } +} + +app.get('/api/system-stats', requireLogin, async (req, res) => { + try { + let dockerAvailable = false; + let containerCount = '0'; + let memoryUsage = '0%'; + let cpuLoad = '0%'; + let diskSpace = '0%'; + let recentActivities = []; + + // 尝试初始化Docker + const docker = await initDocker(); + if (docker) { + dockerAvailable = true; + + // 获取容器统计 + try { + const containers = await docker.listContainers({ all: true }); + containerCount = containers.length.toString(); + + // 获取最近的容器活动 + const runningContainers = containers.filter(c => c.State === 'running'); + for (let i = 0; i < Math.min(3, runningContainers.length); i++) { + recentActivities.push({ + time: new Date(runningContainers[i].Created * 1000).toLocaleString(), + action: '运行中', + container: runningContainers[i].Names[0].replace(/^\//, ''), + status: '正常' + }); + } + } catch (containerError) { + logger.error('获取容器信息失败:', containerError); + } + } + + // 即使Docker不可用,也尝试获取系统信息 + try { + // 获取内存使用情况 + const memInfo = await execPromise('free -m | grep Mem'); + if (memInfo) { + const memParts = memInfo.split(/\s+/); + if (memParts.length >= 3) { + const total = parseInt(memParts[1], 10); + const used = parseInt(memParts[2], 10); + memoryUsage = Math.round((used / total) * 100) + '%'; + } + } + + // 获取CPU负载 + const loadAvg = await execPromise('cat /proc/loadavg'); + if (loadAvg) { + const load = parseFloat(loadAvg.split(' ')[0]); + cpuLoad = (load * 100).toFixed(2) + '%'; + } + + // 获取磁盘空间 + const diskInfo = await execPromise('df -h | grep -E "/$|/home" | head -1'); + if (diskInfo) { + const diskParts = diskInfo.split(/\s+/); + if (diskParts.length >= 5) { + diskSpace = diskParts[4]; // 使用百分比 + } + } + } catch (sysError) { + logger.error('获取系统信息失败:', sysError); + } + + // 如果没有活动记录,添加一个默认记录 + if (recentActivities.length === 0) { + recentActivities.push({ + time: new Date().toLocaleString(), + action: '系统检查', + container: '监控服务', + status: dockerAvailable ? '正常' : 'Docker服务不可用' + }); + } + + // 返回收集到的所有数据,即使部分数据可能不完整 + res.json({ + dockerAvailable, + containerCount, + memoryUsage, + cpuLoad, + diskSpace, + recentActivities + }); + + } catch (error) { + logger.error('获取系统统计数据失败:', error); + + // 即使出错,仍然尝试返回一些基本数据 + res.status(200).json({ + dockerAvailable: false, + containerCount: '0', + memoryUsage: '未知', + cpuLoad: '未知', + diskSpace: '未知', + recentActivities: [{ + time: new Date().toLocaleString(), + action: '系统错误', + container: '监控服务', + status: '数据获取失败' + }], + error: '获取系统统计数据失败', + errorDetails: error.message + }); + } +}); + + +// 辅助函数 +function execPromise(cmd) { + return new Promise((resolve, reject) => { + exec(cmd, (error, stdout, stderr) => { + if (error) { + reject(error); + return; + } + resolve(stdout.trim()); + }); + }); +} + +// 执行系统命令的辅助函数 +async function execCommand(command) { + return new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + reject(error); + return; + } + resolve({ stdout, stderr }); + }); + }); +} + +// API端点:获取用户信息 +app.get('/api/user-info', requireLogin, async (req, res) => { + try { + // 确保用户已登录 + if (!req.session.user) { + return res.status(401).json({ error: '未登录' }); + } + + const users = await readUsers(); + const user = users.users.find(u => u.username === req.session.user.username); + + if (!user) { + return res.status(404).json({ error: '用户不存在' }); + } + + // 计算账户年龄(如果有创建日期) + let accountAge = '0'; + if (user.createdAt) { + const createdDate = new Date(user.createdAt); + const currentDate = new Date(); + const diffTime = Math.abs(currentDate - createdDate); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + accountAge = diffDays.toString(); + } + + // 返回用户信息 + res.json({ + username: user.username, + loginCount: user.loginCount || '0', + lastLogin: user.lastLogin || '无记录', + accountAge + }); + } catch (error) { + logger.error('获取用户信息失败:', error); + res.status(500).json({ error: '获取用户信息失败', details: error.message }); + } +}); + // 启动服务器 const PORT = process.env.PORT || 3000; server.listen(PORT, async () => { @@ -1066,4 +1745,52 @@ server.listen(PORT, async () => { } catch (error) { logger.error('Failed to start monitoring:', error); } -}); \ No newline at end of file +}); + +// 统一的错误处理函数 +function handleAxiosError(error, res, message) { + let errorDetails = ''; + + if (error.response) { + // 服务器响应错误 + const status = error.response.status; + errorDetails = `状态码: ${status}`; + + if (error.response.data && error.response.data.message) { + errorDetails += `, 信息: ${error.response.data.message}`; + } + + console.error(`[ERROR] ${message}: ${errorDetails}`); + res.status(status).json({ + error: `${message} (${errorDetails})`, + details: error.response.data + }); + + } else if (error.request) { + // 请求已发送但没有收到响应 + if (error.code === 'ECONNRESET') { + errorDetails = '连接被重置,这可能是由于网络不稳定或服务端断开连接'; + } else if (error.code === 'ECONNABORTED') { + errorDetails = '请求超时,服务器响应时间过长'; + } else { + errorDetails = `${error.code || '未知错误代码'}: ${error.message}`; + } + + console.error(`[ERROR] ${message}: ${errorDetails}`); + res.status(503).json({ + error: `${message} (${errorDetails})`, + retryable: true + }); + + } else { + // 其他错误 + errorDetails = error.message; + console.error(`[ERROR] ${message}: ${errorDetails}`); + console.error(`[ERROR] 错误堆栈: ${error.stack}`); + + res.status(500).json({ + error: `${message} (${errorDetails})`, + retryable: true + }); + } +} \ No newline at end of file diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index 96363dc..38aeb57 100644 --- a/hubcmdui/web/admin.html +++ b/hubcmdui/web/admin.html @@ -5,125 +5,621 @@ Docker 镜像代理加速 - 管理面板 - + + - - - @@ -833,9 +1219,25 @@