server.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. const express = require('express');
  2. const fs = require('fs').promises;
  3. const path = require('path');
  4. const bodyParser = require('body-parser');
  5. const session = require('express-session');
  6. const bcrypt = require('bcrypt');
  7. const crypto = require('crypto');
  8. const axios = require('axios'); // 用于发送 HTTP 请求
  9. const Docker = require('dockerode');
  10. const app = express();
  11. const cors = require('cors');
  12. const WebSocket = require('ws');
  13. const http = require('http');
  14. const { exec } = require('child_process'); // 网络测试
  15. const validator = require('validator');
  16. const logger = require('./logger');
  17. let docker = null;
  18. async function initDocker() {
  19. if (docker === null) {
  20. docker = new Docker();
  21. try {
  22. await docker.ping();
  23. logger.success('成功连接到 Docker 守护进程');
  24. } catch (err) {
  25. logger.error(`无法连接到 Docker 守护进程: ${err.message}`);
  26. docker = null;
  27. }
  28. }
  29. return docker;
  30. }
  31. app.use(cors());
  32. app.use(express.json());
  33. app.use(express.static('web'));
  34. app.use(bodyParser.urlencoded({ extended: true }));
  35. app.use(session({
  36. secret: 'OhTq3faqSKoxbV%NJV',
  37. resave: false,
  38. saveUninitialized: true,
  39. cookie: { secure: false } // 设置为true如果使用HTTPS
  40. }));
  41. app.use(require('morgan')('dev'));
  42. app.get('/admin', (req, res) => {
  43. res.sendFile(path.join(__dirname, 'web', 'admin.html'));
  44. });
  45. // 新增:Docker Hub 搜索 API
  46. app.get('/api/search', async (req, res) => {
  47. const searchTerm = req.query.term;
  48. if (!searchTerm) {
  49. return res.status(400).json({ error: 'Search term is required' });
  50. }
  51. try {
  52. const response = await axios.get(`https://hub.docker.com/v2/search/repositories/?query=${encodeURIComponent(searchTerm)}`);
  53. res.json(response.data);
  54. } catch (error) {
  55. logger.error('Error searching Docker Hub:', error);
  56. res.status(500).json({ error: 'Failed to search Docker Hub' });
  57. }
  58. });
  59. const CONFIG_FILE = path.join(__dirname, 'config.json');
  60. const USERS_FILE = path.join(__dirname, 'users.json');
  61. const DOCUMENTATION_DIR = path.join(__dirname, 'documentation');
  62. const DOCUMENTATION_FILE = path.join(__dirname, 'documentation.md');
  63. // 读取配置
  64. async function readConfig() {
  65. try {
  66. const data = await fs.readFile(CONFIG_FILE, 'utf8');
  67. let config;
  68. if (!data.trim()) {
  69. config = {
  70. logo: '',
  71. menuItems: [],
  72. adImages: []
  73. };
  74. } else {
  75. config = JSON.parse(data);
  76. }
  77. // 确保 monitoringConfig 存在,如果不存在则添加默认值
  78. if (!config.monitoringConfig) {
  79. config.monitoringConfig = {
  80. webhookUrl: '',
  81. monitorInterval: 60,
  82. isEnabled: false
  83. };
  84. }
  85. return config;
  86. } catch (error) {
  87. logger.error('Failed to read config:', error);
  88. if (error.code === 'ENOENT') {
  89. return {
  90. logo: '',
  91. menuItems: [],
  92. adImages: [],
  93. monitoringConfig: {
  94. webhookUrl: '',
  95. monitorInterval: 60,
  96. isEnabled: false
  97. }
  98. };
  99. }
  100. throw error;
  101. }
  102. }
  103. // 写入配置
  104. async function writeConfig(config) {
  105. try {
  106. await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
  107. logger.success('Config saved successfully');
  108. } catch (error) {
  109. logger.error('Failed to save config:', error);
  110. throw error;
  111. }
  112. }
  113. // 读取用户
  114. async function readUsers() {
  115. try {
  116. const data = await fs.readFile(USERS_FILE, 'utf8');
  117. return JSON.parse(data);
  118. } catch (error) {
  119. if (error.code === 'ENOENT') {
  120. logger.warn('Users file does not exist, creating default user');
  121. const defaultUser = { username: 'root', password: bcrypt.hashSync('admin', 10) };
  122. await writeUsers([defaultUser]);
  123. return { users: [defaultUser] };
  124. }
  125. throw error;
  126. }
  127. }
  128. // 写入用户
  129. async function writeUsers(users) {
  130. await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8');
  131. }
  132. // 确保 documentation 目录存在
  133. async function ensureDocumentationDir() {
  134. try {
  135. await fs.access(DOCUMENTATION_DIR);
  136. } catch (error) {
  137. if (error.code === 'ENOENT') {
  138. await fs.mkdir(DOCUMENTATION_DIR);
  139. } else {
  140. throw error;
  141. }
  142. }
  143. }
  144. // 读取文档
  145. async function readDocumentation() {
  146. try {
  147. await ensureDocumentationDir();
  148. const files = await fs.readdir(DOCUMENTATION_DIR);
  149. const documents = await Promise.all(files.map(async file => {
  150. const filePath = path.join(DOCUMENTATION_DIR, file);
  151. const content = await fs.readFile(filePath, 'utf8');
  152. const doc = JSON.parse(content);
  153. return {
  154. id: path.parse(file).name,
  155. title: doc.title,
  156. content: doc.content,
  157. published: doc.published
  158. };
  159. }));
  160. const publishedDocuments = documents.filter(doc => doc.published);
  161. return publishedDocuments;
  162. } catch (error) {
  163. logger.error('Error reading documentation:', error);
  164. throw error;
  165. }
  166. }
  167. // 写入文档
  168. async function writeDocumentation(content) {
  169. await fs.writeFile(DOCUMENTATION_FILE, content, 'utf8');
  170. }
  171. // 登录验证
  172. app.post('/api/login', async (req, res) => {
  173. const { username, captcha } = req.body;
  174. if (req.session.captcha !== parseInt(captcha)) {
  175. logger.warn(`Captcha verification failed for user: ${username}`);
  176. return res.status(401).json({ error: '验证码错误' });
  177. }
  178. const users = await readUsers();
  179. const user = users.users.find(u => u.username === username);
  180. if (!user) {
  181. logger.warn(`User ${username} not found`);
  182. return res.status(401).json({ error: '用户名或密码错误' });
  183. }
  184. if (bcrypt.compareSync(req.body.password, user.password)) {
  185. req.session.user = { username: user.username };
  186. logger.info(`User ${username} logged in successfully`);
  187. res.json({ success: true });
  188. } else {
  189. logger.warn(`Login failed for user: ${username}`);
  190. res.status(401).json({ error: '用户名或密码错误' });
  191. }
  192. });
  193. // 修改密码
  194. app.post('/api/change-password', async (req, res) => {
  195. if (!req.session.user) {
  196. return res.status(401).json({ error: 'Not logged in' });
  197. }
  198. const { currentPassword, newPassword } = req.body;
  199. const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
  200. if (!passwordRegex.test(newPassword)) {
  201. return res.status(400).json({ error: 'Password must be 8-16 characters long and contain at least one letter, one number, and one special character' });
  202. }
  203. const users = await readUsers();
  204. const user = users.users.find(u => u.username === req.session.user.username);
  205. if (user && bcrypt.compareSync(currentPassword, user.password)) {
  206. user.password = bcrypt.hashSync(newPassword, 10);
  207. await writeUsers(users.users);
  208. res.json({ success: true });
  209. } else {
  210. res.status(401).json({ error: 'Invalid current password' });
  211. }
  212. });
  213. // 需要登录验证的中间件
  214. function requireLogin(req, res, next) {
  215. // 创建一个新的对象,只包含非敏感信息
  216. const sanitizedSession = {
  217. cookie: req.session.cookie,
  218. captcha: req.session.captcha,
  219. user: req.session.user ? { username: req.session.user.username } : undefined
  220. };
  221. logger.info('Session:', JSON.stringify(sanitizedSession, null, 2));
  222. if (req.session.user) {
  223. next();
  224. } else {
  225. logger.warn('用户未登录');
  226. res.status(401).json({ error: 'Not logged in' });
  227. }
  228. }
  229. // API 端点:获取配置
  230. app.get('/api/config', async (req, res) => {
  231. try {
  232. const config = await readConfig();
  233. res.json(config);
  234. } catch (error) {
  235. res.status(500).json({ error: 'Failed to read config' });
  236. }
  237. });
  238. // API 端点:保存配置
  239. app.post('/api/config', requireLogin, async (req, res) => {
  240. try {
  241. const currentConfig = await readConfig();
  242. const newConfig = { ...currentConfig, ...req.body };
  243. await writeConfig(newConfig);
  244. res.json({ success: true });
  245. } catch (error) {
  246. res.status(500).json({ error: 'Failed to save config' });
  247. }
  248. });
  249. // API 端点:检查会话状态
  250. app.get('/api/check-session', (req, res) => {
  251. if (req.session.user) {
  252. res.json({ success: true });
  253. } else {
  254. res.status(401).json({ error: 'Not logged in' });
  255. }
  256. });
  257. // API 端点:生成验证码
  258. app.get('/api/captcha', (req, res) => {
  259. const num1 = Math.floor(Math.random() * 10);
  260. const num2 = Math.floor(Math.random() * 10);
  261. const captcha = `${num1} + ${num2} = ?`;
  262. req.session.captcha = num1 + num2;
  263. res.json({ captcha });
  264. });
  265. // API端点:获取文档列表
  266. app.get('/api/documentation-list', requireLogin, async (req, res) => {
  267. try {
  268. const files = await fs.readdir(DOCUMENTATION_DIR);
  269. const documents = await Promise.all(files.map(async file => {
  270. const content = await fs.readFile(path.join(DOCUMENTATION_DIR, file), 'utf8');
  271. const doc = JSON.parse(content);
  272. return { id: path.parse(file).name, ...doc };
  273. }));
  274. res.json(documents);
  275. } catch (error) {
  276. res.status(500).json({ error: '读取文档列表失败' });
  277. }
  278. });
  279. // API端点:保存文档
  280. app.post('/api/documentation', requireLogin, async (req, res) => {
  281. try {
  282. const { id, title, content } = req.body;
  283. const docId = id || Date.now().toString();
  284. const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
  285. await fs.writeFile(docPath, JSON.stringify({ title, content, published: false }));
  286. res.json({ success: true });
  287. } catch (error) {
  288. res.status(500).json({ error: '保存文档失败' });
  289. }
  290. });
  291. // API端点:删除文档
  292. app.delete('/api/documentation/:id', requireLogin, async (req, res) => {
  293. try {
  294. const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
  295. await fs.unlink(docPath);
  296. res.json({ success: true });
  297. } catch (error) {
  298. res.status(500).json({ error: '删除文档失败' });
  299. }
  300. });
  301. // API端点:切换文档发布状态
  302. app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res) => {
  303. try {
  304. const docPath = path.join(DOCUMENTATION_DIR, `${req.params.id}.json`);
  305. const content = await fs.readFile(docPath, 'utf8');
  306. const doc = JSON.parse(content);
  307. doc.published = !doc.published;
  308. await fs.writeFile(docPath, JSON.stringify(doc));
  309. res.json({ success: true });
  310. } catch (error) {
  311. res.status(500).json({ error: '更改发布状态失败' });
  312. }
  313. });
  314. // API端点:获取文档
  315. app.get('/api/documentation', async (req, res) => {
  316. try {
  317. const documents = await readDocumentation();
  318. res.json(documents);
  319. } catch (error) {
  320. logger.error('Error in /api/documentation:', error);
  321. res.status(500).json({ error: '读取文档失败', details: error.message });
  322. }
  323. });
  324. // API端点:保存文档
  325. app.post('/api/documentation', requireLogin, async (req, res) => {
  326. try {
  327. const { content } = req.body;
  328. await writeDocumentation(content);
  329. res.json({ success: true });
  330. } catch (error) {
  331. res.status(500).json({ error: '保存文档失败' });
  332. }
  333. });
  334. // 获取文档列表函数
  335. async function getDocumentList() {
  336. try {
  337. await ensureDocumentationDir();
  338. const files = await fs.readdir(DOCUMENTATION_DIR);
  339. logger.info('Files in documentation directory:', files);
  340. const documents = await Promise.all(files.map(async file => {
  341. try {
  342. const filePath = path.join(DOCUMENTATION_DIR, file);
  343. const content = await fs.readFile(filePath, 'utf8');
  344. return {
  345. id: path.parse(file).name,
  346. title: path.parse(file).name, // 使用文件名作为标题
  347. content: content,
  348. published: true // 假设所有文档都是已发布的
  349. };
  350. } catch (fileError) {
  351. logger.error(`Error reading file ${file}:`, fileError);
  352. return null;
  353. }
  354. }));
  355. const validDocuments = documents.filter(doc => doc !== null);
  356. logger.info('Valid documents:', validDocuments);
  357. return validDocuments;
  358. } catch (error) {
  359. logger.error('Error reading document list:', error);
  360. throw error; // 重新抛出错误,让上层函数处理
  361. }
  362. }
  363. app.get('/api/documentation-list', async (req, res) => {
  364. try {
  365. const documents = await getDocumentList();
  366. res.json(documents);
  367. } catch (error) {
  368. logger.error('Error in /api/documentation-list:', error);
  369. res.status(500).json({
  370. error: '读取文档列表失败',
  371. details: error.message,
  372. stack: error.stack
  373. });
  374. }
  375. });
  376. app.get('/api/documentation/:id', async (req, res) => {
  377. try {
  378. const docId = req.params.id;
  379. const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
  380. const content = await fs.readFile(docPath, 'utf8');
  381. const doc = JSON.parse(content);
  382. res.json(doc);
  383. } catch (error) {
  384. logger.error('Error reading document:', error);
  385. res.status(500).json({ error: '读取文档失败', details: error.message });
  386. }
  387. });
  388. // API端点来获取Docker容器状态
  389. app.get('/api/docker-status', requireLogin, async (req, res) => {
  390. try {
  391. const docker = await initDocker();
  392. if (!docker) {
  393. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  394. }
  395. const containers = await docker.listContainers({ all: true });
  396. const containerStatus = await Promise.all(containers.map(async (container) => {
  397. const containerInfo = await docker.getContainer(container.Id).inspect();
  398. const stats = await docker.getContainer(container.Id).stats({ stream: false });
  399. // 计算 CPU 使用率
  400. const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
  401. const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
  402. const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
  403. // 计算内存使用率
  404. const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
  405. return {
  406. id: container.Id.slice(0, 12),
  407. name: container.Names[0].replace(/^\//, ''),
  408. image: container.Image,
  409. state: containerInfo.State.Status,
  410. status: container.Status,
  411. cpu: cpuUsage.toFixed(2) + '%',
  412. memory: memoryUsage.toFixed(2) + '%',
  413. created: new Date(container.Created * 1000).toLocaleString()
  414. };
  415. }));
  416. res.json(containerStatus);
  417. } catch (error) {
  418. logger.error('获取 Docker 状态时出错:', error);
  419. res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
  420. }
  421. });
  422. // API端点:重启容器
  423. app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
  424. try {
  425. const docker = await initDocker();
  426. if (!docker) {
  427. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  428. }
  429. const container = docker.getContainer(req.params.id);
  430. await container.restart();
  431. res.json({ success: true });
  432. } catch (error) {
  433. logger.error('重启容器失败:', error);
  434. res.status(500).json({ error: '重启容器失败', details: error.message });
  435. }
  436. });
  437. // API端点:停止容器
  438. app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
  439. try {
  440. const docker = await initDocker();
  441. if (!docker) {
  442. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  443. }
  444. const container = docker.getContainer(req.params.id);
  445. await container.stop();
  446. res.json({ success: true });
  447. } catch (error) {
  448. logger.error('停止容器失败:', error);
  449. res.status(500).json({ error: '停止容器失败', details: error.message });
  450. }
  451. });
  452. // API端点:获取单个容器的状态
  453. app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
  454. try {
  455. const docker = await initDocker();
  456. if (!docker) {
  457. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  458. }
  459. const container = docker.getContainer(req.params.id);
  460. const containerInfo = await container.inspect();
  461. res.json({ state: containerInfo.State.Status });
  462. } catch (error) {
  463. logger.error('获取容器状态失败:', error);
  464. res.status(500).json({ error: '获取容器状态失败', details: error.message });
  465. }
  466. });
  467. // API端点:更新容器
  468. app.post('/api/docker/update/:id', requireLogin, async (req, res) => {
  469. try {
  470. const docker = await initDocker();
  471. if (!docker) {
  472. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  473. }
  474. const container = docker.getContainer(req.params.id);
  475. const containerInfo = await container.inspect();
  476. const currentImage = containerInfo.Config.Image;
  477. const [imageName] = currentImage.split(':');
  478. const newImage = `${imageName}:${req.body.tag}`;
  479. const containerName = containerInfo.Name.slice(1); // 去掉开头的 '/'
  480. logger.info(`Updating container ${req.params.id} from ${currentImage} to ${newImage}`);
  481. // 拉取新镜像
  482. logger.info(`Pulling new image: ${newImage}`);
  483. await new Promise((resolve, reject) => {
  484. docker.pull(newImage, (err, stream) => {
  485. if (err) return reject(err);
  486. docker.modem.followProgress(stream, (err, output) => err ? reject(err) : resolve(output));
  487. });
  488. });
  489. // 停止旧容器
  490. logger.info('Stopping old container');
  491. await container.stop();
  492. // 删除旧容器
  493. logger.info('Removing old container');
  494. await container.remove();
  495. // 创建新容器
  496. logger.info('Creating new container');
  497. const newContainerConfig = {
  498. ...containerInfo.Config,
  499. Image: newImage,
  500. HostConfig: containerInfo.HostConfig,
  501. NetworkingConfig: {
  502. EndpointsConfig: containerInfo.NetworkSettings.Networks
  503. }
  504. };
  505. const newContainer = await docker.createContainer({
  506. ...newContainerConfig,
  507. name: containerName
  508. });
  509. // 启动新容器
  510. logger.info('Starting new container');
  511. await newContainer.start();
  512. logger.success('Container update completed successfully');
  513. res.json({ success: true, message: '容器更新成功' });
  514. } catch (error) {
  515. logger.error('更新容器失败:', error);
  516. res.status(500).json({ error: '更新容器失败', details: error.message, stack: error.stack });
  517. }
  518. });
  519. // API端点:获取容器日志
  520. app.get('/api/docker/logs/:id', requireLogin, async (req, res) => {
  521. try {
  522. const docker = await initDocker();
  523. if (!docker) {
  524. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  525. }
  526. const container = docker.getContainer(req.params.id);
  527. const logs = await container.logs({
  528. stdout: true,
  529. stderr: true,
  530. tail: 100, // 获取最后100行日志
  531. follow: false
  532. });
  533. res.send(logs);
  534. } catch (error) {
  535. logger.error('获取容器日志失败:', error);
  536. res.status(500).json({ error: '获取容器日志失败', details: error.message });
  537. }
  538. });
  539. const server = http.createServer(app);
  540. const wss = new WebSocket.Server({ server });
  541. wss.on('connection', (ws, req) => {
  542. const containerId = req.url.split('/').pop();
  543. const docker = new Docker();
  544. const container = docker.getContainer(containerId);
  545. container.logs({
  546. follow: true,
  547. stdout: true,
  548. stderr: true,
  549. tail: 100
  550. }, (err, stream) => {
  551. if (err) {
  552. ws.send('Error: ' + err.message);
  553. return;
  554. }
  555. stream.on('data', (chunk) => {
  556. // 移除 ANSI 转义序列
  557. const cleanedChunk = chunk.toString('utf8').replace(/\x1B\[[0-9;]*[JKmsu]/g, '');
  558. // 移除不可打印字符
  559. const printableChunk = cleanedChunk.replace(/[^\x20-\x7E\x0A\x0D]/g, '');
  560. ws.send(printableChunk);
  561. });
  562. ws.on('close', () => {
  563. stream.destroy();
  564. });
  565. });
  566. });
  567. // API端点:删除容器
  568. app.post('/api/docker/delete/:id', requireLogin, async (req, res) => {
  569. try {
  570. const docker = await initDocker();
  571. if (!docker) {
  572. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  573. }
  574. const container = docker.getContainer(req.params.id);
  575. // 首先停止容器(如果正在运行)
  576. try {
  577. await container.stop();
  578. } catch (stopError) {
  579. logger.info('Container may already be stopped:', stopError.message);
  580. }
  581. // 然后删除容器
  582. await container.remove();
  583. res.json({ success: true, message: '容器已成功删除' });
  584. } catch (error) {
  585. logger.error('删除容器失败:', error);
  586. res.status(500).json({ error: '删除容器失败', details: error.message });
  587. }
  588. });
  589. // 网络测试
  590. const { execSync } = require('child_process');
  591. // 在应用启动时执行
  592. const pingPath = execSync('which ping').toString().trim();
  593. const traceroutePath = execSync('which traceroute').toString().trim();
  594. app.post('/api/network-test', requireLogin, (req, res) => {
  595. const { domain, testType } = req.body;
  596. let command;
  597. switch (testType) {
  598. case 'ping':
  599. command = `${pingPath} -c 4 ${domain}`;
  600. break;
  601. case 'traceroute':
  602. command = `${traceroutePath} -m 10 ${domain}`;
  603. break;
  604. default:
  605. return res.status(400).send('无效的测试类型');
  606. }
  607. exec(command, { timeout: 30000 }, (error, stdout, stderr) => {
  608. if (error) {
  609. logger.error(`执行出错: ${error}`);
  610. return res.status(500).send('测试执行失败');
  611. }
  612. res.send(stdout || stderr);
  613. });
  614. });
  615. // docker 监控
  616. app.get('/api/monitoring-config', requireLogin, async (req, res) => {
  617. try {
  618. const config = await readConfig();
  619. res.json({
  620. webhookUrl: config.monitoringConfig.webhookUrl,
  621. monitorInterval: config.monitoringConfig.monitorInterval,
  622. isEnabled: config.monitoringConfig.isEnabled
  623. });
  624. } catch (error) {
  625. logger.error('Failed to get monitoring config:', error);
  626. res.status(500).json({ error: 'Failed to get monitoring config', details: error.message });
  627. }
  628. });
  629. app.post('/api/monitoring-config', requireLogin, async (req, res) => {
  630. try {
  631. const { webhookUrl, monitorInterval, isEnabled } = req.body;
  632. const config = await readConfig();
  633. config.monitoringConfig = { webhookUrl, monitorInterval: parseInt(monitorInterval), isEnabled };
  634. await writeConfig(config);
  635. if (isEnabled) {
  636. await startMonitoring();
  637. } else {
  638. clearInterval(monitoringInterval);
  639. monitoringInterval = null;
  640. }
  641. res.json({ success: true });
  642. } catch (error) {
  643. logger.error('Failed to save monitoring config:', error);
  644. res.status(500).json({ error: 'Failed to save monitoring config', details: error.message });
  645. }
  646. });
  647. let monitoringInterval;
  648. // 用于跟踪已发送的告警
  649. let sentAlerts = new Set();
  650. // 发送告警的函数,包含重试逻辑
  651. async function sendAlertWithRetry(webhookUrl, containerName, status, maxRetries = 6) {
  652. // 移除容器名称前面的斜杠
  653. const cleanContainerName = containerName.replace(/^\//, '');
  654. for (let attempt = 1; attempt <= maxRetries; attempt++) {
  655. try {
  656. const response = await axios.post(webhookUrl, {
  657. msgtype: 'text',
  658. text: {
  659. content: `警告: 容器 ${cleanContainerName} ${status}`
  660. }
  661. }, {
  662. timeout: 5000
  663. });
  664. if (response.status === 200 && response.data.errcode === 0) {
  665. logger.success(`告警发送成功: ${cleanContainerName} ${status}`);
  666. return;
  667. } else {
  668. throw new Error(`请求成功但返回错误:${response.data.errmsg}`);
  669. }
  670. } catch (error) {
  671. if (attempt === maxRetries) {
  672. logger.error(`达到最大重试次数,放弃发送告警: ${cleanContainerName} ${status}`);
  673. return;
  674. }
  675. await new Promise(resolve => setTimeout(resolve, 10000));
  676. }
  677. }
  678. }
  679. let containerStates = new Map();
  680. let lastStopAlertTime = new Map();
  681. let secondAlertSent = new Set();
  682. let lastAlertTime = new Map();
  683. async function startMonitoring() {
  684. const config = await readConfig();
  685. const { webhookUrl, monitorInterval, isEnabled } = config.monitoringConfig || {};
  686. if (isEnabled && webhookUrl) {
  687. const docker = await initDocker();
  688. if (docker) {
  689. await initializeContainerStates(docker);
  690. const dockerEventStream = await docker.getEvents();
  691. dockerEventStream.on('data', async (chunk) => {
  692. const event = JSON.parse(chunk.toString());
  693. if (event.Type === 'container' && (event.Action === 'start' || event.Action === 'die')) {
  694. await handleContainerEvent(docker, event, webhookUrl);
  695. }
  696. });
  697. monitoringInterval = setInterval(async () => {
  698. await checkContainerStates(docker, webhookUrl);
  699. }, (monitorInterval || 60) * 1000);
  700. }
  701. } else if (monitoringInterval) {
  702. clearInterval(monitoringInterval);
  703. monitoringInterval = null;
  704. }
  705. }
  706. async function initializeContainerStates(docker) {
  707. const containers = await docker.listContainers({ all: true });
  708. for (const container of containers) {
  709. const containerInfo = await docker.getContainer(container.Id).inspect();
  710. containerStates.set(container.Id, containerInfo.State.Status);
  711. }
  712. }
  713. async function handleContainerEvent(docker, event, webhookUrl) {
  714. const containerId = event.Actor.ID;
  715. const container = docker.getContainer(containerId);
  716. const containerInfo = await container.inspect();
  717. const newStatus = containerInfo.State.Status;
  718. const oldStatus = containerStates.get(containerId);
  719. if (oldStatus && oldStatus !== newStatus) {
  720. if (newStatus === 'running') {
  721. // 容器恢复到 running 状态时立即发送告警
  722. await sendAlertWithRetry(webhookUrl, containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
  723. lastStopAlertTime.delete(containerInfo.Name); // 清除停止告警时间
  724. secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
  725. } else if (oldStatus === 'running') {
  726. // 容器从 running 状态变为其他状态时发送告警
  727. await sendAlertWithRetry(webhookUrl, containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
  728. lastStopAlertTime.set(containerInfo.Name, Date.now()); // 记录停止告警时间
  729. secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
  730. }
  731. containerStates.set(containerId, newStatus);
  732. }
  733. }
  734. async function checkContainerStates(docker, webhookUrl) {
  735. const containers = await docker.listContainers({ all: true });
  736. for (const container of containers) {
  737. const containerInfo = await docker.getContainer(container.Id).inspect();
  738. const newStatus = containerInfo.State.Status;
  739. const oldStatus = containerStates.get(container.Id);
  740. if (oldStatus && oldStatus !== newStatus) {
  741. if (newStatus === 'running') {
  742. // 容器恢复到 running 状态时立即发送告警
  743. await sendAlertWithRetry(webhookUrl, containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
  744. lastStopAlertTime.delete(containerInfo.Name); // 清除停止告警时间
  745. secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
  746. } else if (oldStatus === 'running') {
  747. // 容器从 running 状态变为其他状态时发送告警
  748. await sendAlertWithRetry(webhookUrl, containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
  749. lastStopAlertTime.set(containerInfo.Name, Date.now()); // 记录停止告警时间
  750. secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
  751. }
  752. containerStates.set(container.Id, newStatus);
  753. } else if (newStatus !== 'running') {
  754. // 检查是否需要发送第二次停止告警
  755. await checkSecondStopAlert(webhookUrl, containerInfo.Name, newStatus);
  756. }
  757. }
  758. }
  759. async function checkRepeatStopAlert(webhookUrl, containerName, currentStatus) {
  760. const now = Date.now();
  761. const lastStopAlert = lastStopAlertTime.get(containerName) || 0;
  762. // 如果距离上次停止告警超过1小时,再次发送告警
  763. if (now - lastStopAlert >= 60 * 60 * 1000) {
  764. await sendAlertWithRetry(webhookUrl, containerName, `仍未恢复 (当前状态: ${currentStatus})`);
  765. lastStopAlertTime.set(containerName, now); // 更新停止告警时间
  766. }
  767. }
  768. async function checkSecondStopAlert(webhookUrl, containerName, currentStatus) {
  769. const now = Date.now();
  770. const lastStopAlert = lastStopAlertTime.get(containerName) || 0;
  771. // 如果距离上次停止告警超过1小时,且还没有发送过第二次告警,则发送第二次告警
  772. if (now - lastStopAlert >= 60 * 60 * 1000 && !secondAlertSent.has(containerName)) {
  773. await sendAlertWithRetry(webhookUrl, containerName, `仍未恢复 (当前状态: ${currentStatus})`);
  774. secondAlertSent.add(containerName); // 标记已发送第二次告警
  775. }
  776. }
  777. async function sendAlert(webhookUrl, containerName, status) {
  778. try {
  779. await axios.post(webhookUrl, {
  780. msgtype: 'text',
  781. text: {
  782. content: `警告: 容器 ${containerName} 当前状态为 ${status}`
  783. }
  784. });
  785. } catch (error) {
  786. logger.error('发送告警失败:', error);
  787. }
  788. }
  789. // API端点:切换监控状态
  790. app.post('/api/toggle-monitoring', requireLogin, async (req, res) => {
  791. try {
  792. const { isEnabled } = req.body;
  793. const config = await readConfig();
  794. config.monitoringConfig.isEnabled = isEnabled;
  795. await writeConfig(config);
  796. if (isEnabled) {
  797. await startMonitoring();
  798. } else {
  799. clearInterval(monitoringInterval);
  800. monitoringInterval = null;
  801. }
  802. res.json({ success: true, message: `Monitoring ${isEnabled ? 'enabled' : 'disabled'}` });
  803. } catch (error) {
  804. logger.error('Failed to toggle monitoring:', error);
  805. res.status(500).json({ error: 'Failed to toggle monitoring', details: error.message });
  806. }
  807. });
  808. app.get('/api/stopped-containers', requireLogin, async (req, res) => {
  809. try {
  810. const docker = await initDocker();
  811. if (!docker) {
  812. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  813. }
  814. const containers = await docker.listContainers({ all: true });
  815. const stoppedContainers = containers
  816. .filter(container => container.State !== 'running')
  817. .map(container => ({
  818. id: container.Id.slice(0, 12),
  819. name: container.Names[0].replace(/^\//, ''),
  820. status: container.State
  821. }));
  822. res.json(stoppedContainers);
  823. } catch (error) {
  824. logger.error('获取已停止容器列表失败:', error);
  825. res.status(500).json({ error: '获取已停止容器列表失败', details: error.message });
  826. }
  827. });
  828. async function loadMonitoringConfig() {
  829. try {
  830. const response = await fetch('/api/monitoring-config');
  831. const config = await response.json();
  832. document.getElementById('webhookUrl').value = config.webhookUrl || '';
  833. document.getElementById('monitorInterval').value = config.monitorInterval || 60;
  834. updateMonitoringStatus(config.isEnabled);
  835. // 添加实时状态检查
  836. const statusResponse = await fetch('/api/monitoring-status');
  837. const statusData = await statusResponse.json();
  838. updateMonitoringStatus(statusData.isRunning);
  839. } catch (error) {
  840. showMessage('加载监控配置失败: ' + error.message, true);
  841. }
  842. }
  843. app.get('/api/monitoring-status', requireLogin, (req, res) => {
  844. res.json({ isRunning: !!monitoringInterval });
  845. });
  846. app.get('/api/refresh-stopped-containers', requireLogin, async (req, res) => {
  847. try {
  848. const docker = await initDocker();
  849. if (!docker) {
  850. return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
  851. }
  852. const containers = await docker.listContainers({ all: true });
  853. const stoppedContainers = containers
  854. .filter(container => container.State !== 'running')
  855. .map(container => ({
  856. id: container.Id.slice(0, 12),
  857. name: container.Names[0].replace(/^\//, ''),
  858. status: container.State
  859. }));
  860. res.json(stoppedContainers);
  861. } catch (error) {
  862. logger.error('刷新已停止容器列表失败:', error);
  863. res.status(500).json({ error: '刷新已停止容器列表失败', details: error.message });
  864. }
  865. });
  866. async function refreshStoppedContainers() {
  867. try {
  868. const response = await fetch('/api/refresh-stopped-containers');
  869. if (!response.ok) {
  870. throw new Error('Failed to fetch stopped containers');
  871. }
  872. const containers = await response.json();
  873. renderStoppedContainers(containers);
  874. showMessage('已停止的容器状态已刷新', false);
  875. } catch (error) {
  876. console.error('Error refreshing stopped containers:', error);
  877. showMessage('刷新已停止的容器状态失败: ' + error.message, true);
  878. }
  879. }
  880. // 导出函数以供其他模块使用
  881. module.exports = {
  882. startMonitoring,
  883. sendAlertWithRetry
  884. };
  885. // 启动服务器
  886. const PORT = process.env.PORT || 3000;
  887. server.listen(PORT, async () => {
  888. logger.info(`Server is running on http://localhost:${PORT}`);
  889. try {
  890. await startMonitoring();
  891. } catch (error) {
  892. logger.error('Failed to start monitoring:', error);
  893. }
  894. });