浏览代码

feat: Add Telegram alert notification type, add alert testing functionality.

dqzboy 1 年之前
父节点
当前提交
27a51b8a70
共有 6 个文件被更改,包括 198 次插入62 次删除
  1. 1 1
      README.md
  2. 1 1
      hubcmdui/README.md
  3. 4 1
      hubcmdui/config.json
  4. 0 0
      hubcmdui/documentation/1724594777670.json
  5. 95 49
      hubcmdui/server.js
  6. 97 10
      hubcmdui/web/admin.html

+ 1 - 1
README.md

@@ -239,7 +239,7 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1
     </tr>
     <tr>
         <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/8569c5c4-4ce6-4cd4-8547-fa9816019049?raw=true"></td>
-        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033?raw=true"></td>
+        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/fb30f747-a2af-4fc8-b3cc-05c71a044da0?raw=true"></td>
     </tr>
 </table>
 

+ 1 - 1
hubcmdui/README.md

@@ -141,7 +141,7 @@ docker logs -f [容器ID或名称]
 
 <table>
     <tr>
-        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/34aa808b-2352-4a0c-80d2-88251275495c"?raw=true"></td>
+        <td width="50%" align="center"><img src="https://github.com/user-attachments/assets/7a63c464-adde-4775-9a30-d70b14aa0d1e"?raw=true"></td>
     </tr>
 </table>
 

+ 4 - 1
hubcmdui/config.json

@@ -28,7 +28,10 @@
   ],
   "proxyDomain": "dqzboy.github.io",
   "monitoringConfig": {
-    "webhookUrl": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=",
+    "notificationType": "telegram",
+    "webhookUrl": "",
+    "telegramToken": "",
+    "telegramChatId": "",
     "monitorInterval": 60,
     "isEnabled": true
   }

文件差异内容过多而无法显示
+ 0 - 0
hubcmdui/documentation/1724594777670.json


+ 95 - 49
hubcmdui/server.js

@@ -691,7 +691,10 @@ app.get('/api/monitoring-config', requireLogin, async (req, res) => {
   try {
     const config = await readConfig();
     res.json({
+      notificationType: config.monitoringConfig.notificationType || 'wechat',
       webhookUrl: config.monitoringConfig.webhookUrl,
+      telegramToken: config.monitoringConfig.telegramToken,
+      telegramChatId: config.monitoringConfig.telegramChatId,
       monitorInterval: config.monitoringConfig.monitorInterval,
       isEnabled: config.monitoringConfig.isEnabled
     });
@@ -703,17 +706,20 @@ app.get('/api/monitoring-config', requireLogin, async (req, res) => {
 
 app.post('/api/monitoring-config', requireLogin, async (req, res) => {
   try {
-    const { webhookUrl, monitorInterval, isEnabled } = req.body;
+    const { notificationType, webhookUrl, telegramToken, telegramChatId, monitorInterval, isEnabled } = req.body;
     const config = await readConfig();
-    config.monitoringConfig = { webhookUrl, monitorInterval: parseInt(monitorInterval), isEnabled };
+    config.monitoringConfig = { 
+      notificationType,
+      webhookUrl,
+      telegramToken,
+      telegramChatId,
+      monitorInterval: parseInt(monitorInterval), 
+      isEnabled 
+    };
     await writeConfig(config);
 
-    if (isEnabled) {
-      await startMonitoring();
-    } else {
-      clearInterval(monitoringInterval);
-      monitoringInterval = null;
-    }
+    // 重新启动监控
+    await startMonitoring();
 
     res.json({ success: true });
   } catch (error) {
@@ -727,27 +733,20 @@ let monitoringInterval;
 let sentAlerts = new Set();
 
 // 发送告警的函数,包含重试逻辑
-async function sendAlertWithRetry(webhookUrl, containerName, status, maxRetries = 6) {
-  // 移除容器名称前面的斜杠
+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 {
-      const response = await axios.post(webhookUrl, {
-        msgtype: 'text',
-        text: {
-          content: `警告: 容器 ${cleanContainerName} ${status}`
-        }
-      }, {
-        timeout: 5000
-      });
-
-      if (response.status === 200 && response.data.errcode === 0) {
-        logger.success(`告警发送成功: ${cleanContainerName} ${status}`);
-        return;
-      } else {
-        throw new Error(`请求成功但返回错误:${response.data.errmsg}`);
+      if (notificationType === 'wechat') {
+        await sendWechatAlert(webhookUrl, cleanContainerName, status);
+      } else if (notificationType === 'telegram') {
+        await sendTelegramAlert(telegramToken, telegramChatId, cleanContainerName, status);
       }
+      logger.success(`告警发送成功: ${cleanContainerName} ${status}`);
+      return;
     } catch (error) {
       if (attempt === maxRetries) {
         logger.error(`达到最大重试次数,放弃发送告警: ${cleanContainerName} ${status}`);
@@ -758,6 +757,54 @@ async function sendAlertWithRetry(webhookUrl, containerName, status, maxRetries
   }
 }
 
+async function sendWechatAlert(webhookUrl, containerName, status) {
+  const response = await axios.post(webhookUrl, {
+    msgtype: 'text',
+    text: {
+      content: `通知: 容器 ${containerName} ${status}`
+    }
+  }, {
+    timeout: 5000
+  });
+
+  if (response.status !== 200 || response.data.errcode !== 0) {
+    throw new Error(`请求成功但返回错误:${response.data.errmsg}`);
+  }
+}
+
+async function sendTelegramAlert(token, chatId, containerName, status) {
+  const url = `https://api.telegram.org/bot${token}/sendMessage`;
+  const response = await axios.post(url, {
+    chat_id: chatId,
+    text: `通知: 容器 ${containerName} ${status}`
+  }, {
+    timeout: 5000
+  });
+
+  if (response.status !== 200 || !response.data.ok) {
+    throw new Error(`发送Telegram消息失败:${JSON.stringify(response.data)}`);
+  }
+}
+
+app.post('/api/test-notification', requireLogin, async (req, res) => {
+  try {
+    const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body;
+    
+    if (notificationType === 'wechat') {
+      await sendWechatAlert(webhookUrl, 'Test Container', 'This is a test notification');
+    } else if (notificationType === 'telegram') {
+      await sendTelegramAlert(telegramToken, telegramChatId, 'Test Container', 'This is a test notification');
+    } else {
+      throw new Error('Unsupported notification type');
+    }
+
+    res.json({ success: true, message: 'Test notification sent successfully' });
+  } catch (error) {
+    logger.error('Failed to send test notification:', error);
+    res.status(500).json({ error: 'Failed to send test notification', details: error.message });
+  }
+});
+
 let containerStates = new Map();
 let lastStopAlertTime = new Map();
 let secondAlertSent = new Set();
@@ -765,24 +812,28 @@ let lastAlertTime = new Map();
 
 async function startMonitoring() {
   const config = await readConfig();
-  const { webhookUrl, monitorInterval, isEnabled } = config.monitoringConfig || {};
+  const { notificationType, webhookUrl, telegramToken, telegramChatId, monitorInterval, isEnabled } = config.monitoringConfig || {};
 
-  if (isEnabled && webhookUrl) {
+  if (isEnabled) {
     const docker = await initDocker();
     if (docker) {
       await initializeContainerStates(docker);
 
+      if (monitoringInterval) {
+        clearInterval(monitoringInterval);
+      }
+
       const dockerEventStream = await docker.getEvents();
 
       dockerEventStream.on('data', async (chunk) => {
         const event = JSON.parse(chunk.toString());
         if (event.Type === 'container' && (event.Action === 'start' || event.Action === 'die')) {
-          await handleContainerEvent(docker, event, webhookUrl);
+          await handleContainerEvent(docker, event, config.monitoringConfig);
         }
       });
 
       monitoringInterval = setInterval(async () => {
-        await checkContainerStates(docker, webhookUrl);
+        await checkContainerStates(docker, config.monitoringConfig);
       }, (monitorInterval || 60) * 1000);
     }
   } else if (monitoringInterval) {
@@ -801,7 +852,7 @@ async function initializeContainerStates(docker) {
 }
 
 
-async function handleContainerEvent(docker, event, webhookUrl) {
+async function handleContainerEvent(docker, event, monitoringConfig) {
   const containerId = event.Actor.ID;
   const container = docker.getContainer(containerId);
   const containerInfo = await container.inspect();
@@ -810,21 +861,19 @@ async function handleContainerEvent(docker, event, webhookUrl) {
 
   if (oldStatus && oldStatus !== newStatus) {
     if (newStatus === 'running') {
-      // 容器恢复到 running 状态时立即发送告警
-      await sendAlertWithRetry(webhookUrl, containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
-      lastStopAlertTime.delete(containerInfo.Name); // 清除停止告警时间
-      secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
+      await sendAlertWithRetry(containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
+      lastStopAlertTime.delete(containerInfo.Name);
+      secondAlertSent.delete(containerInfo.Name);
     } else if (oldStatus === 'running') {
-      // 容器从 running 状态变为其他状态时发送告警
-      await sendAlertWithRetry(webhookUrl, containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
-      lastStopAlertTime.set(containerInfo.Name, Date.now()); // 记录停止告警时间
-      secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
+      await sendAlertWithRetry(containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
+      lastStopAlertTime.set(containerInfo.Name, Date.now());
+      secondAlertSent.delete(containerInfo.Name);
     }
     containerStates.set(containerId, newStatus);
   }
 }
 
-async function checkContainerStates(docker, webhookUrl) {
+async function checkContainerStates(docker, monitoringConfig) {
   const containers = await docker.listContainers({ all: true });
   for (const container of containers) {
     const containerInfo = await docker.getContainer(container.Id).inspect();
@@ -833,20 +882,17 @@ async function checkContainerStates(docker, webhookUrl) {
     
     if (oldStatus && oldStatus !== newStatus) {
       if (newStatus === 'running') {
-        // 容器恢复到 running 状态时立即发送告警
-        await sendAlertWithRetry(webhookUrl, containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
-        lastStopAlertTime.delete(containerInfo.Name); // 清除停止告警时间
-        secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
+        await sendAlertWithRetry(containerInfo.Name, `恢复运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
+        lastStopAlertTime.delete(containerInfo.Name);
+        secondAlertSent.delete(containerInfo.Name);
       } else if (oldStatus === 'running') {
-        // 容器从 running 状态变为其他状态时发送告警
-        await sendAlertWithRetry(webhookUrl, containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`);
-        lastStopAlertTime.set(containerInfo.Name, Date.now()); // 记录停止告警时间
-        secondAlertSent.delete(containerInfo.Name); // 清除二次告警标记
+        await sendAlertWithRetry(containerInfo.Name, `停止运行 (之前状态: ${oldStatus}, 当前状态: ${newStatus})`, monitoringConfig);
+        lastStopAlertTime.set(containerInfo.Name, Date.now());
+        secondAlertSent.delete(containerInfo.Name);
       }
       containerStates.set(container.Id, newStatus);
     } else if (newStatus !== 'running') {
-      // 检查是否需要发送第二次停止告警
-      await checkSecondStopAlert(webhookUrl, containerInfo.Name, newStatus);
+      await checkSecondStopAlert(containerInfo.Name, newStatus, monitoringConfig);
     }
   }
 }
@@ -879,7 +925,7 @@ async function sendAlert(webhookUrl, containerName, status) {
     await axios.post(webhookUrl, {
       msgtype: 'text',
       text: {
-        content: `告: 容器 ${containerName} 当前状态为 ${status}`
+        content: `告警通知: 容器 ${containerName} 当前状态为 ${status}`
       }
     });
   } catch (error) {

+ 97 - 10
hubcmdui/web/admin.html

@@ -752,14 +752,34 @@
                     </div>
                     <div class="config-form">
                         <div class="form-group">
-                            <label for="webhookUrl">企业微信机器人 Webhook URL:</label>
-                            <input type="text" id="webhookUrl" name="webhookUrl" class="form-control">
+                            <label for="notificationType">通知方式:</label>
+                            <select id="notificationType" class="form-control" onchange="toggleNotificationFields()">
+                                <option value="wechat">企业微信群机器人</option>
+                                <option value="telegram">Telegram Bot</option>
+                            </select>
+                        </div>
+                        <div id="wechatFields">
+                            <div class="form-group">
+                                <label for="webhookUrl">企业微信机器人 Webhook URL:</label>
+                                <input type="text" id="webhookUrl" name="webhookUrl" class="form-control">
+                            </div>
+                        </div>
+                        <div id="telegramFields" style="display: none;">
+                            <div class="form-group">
+                                <label for="telegramToken">Telegram Bot Token:</label>
+                                <input type="text" id="telegramToken" name="telegramToken" class="form-control">
+                            </div>
+                            <div class="form-group">
+                                <label for="telegramChatId">Telegram Chat ID:</label>
+                                <input type="text" id="telegramChatId" name="telegramChatId" class="form-control">
+                            </div>
                         </div>
                         <div class="form-group">
                             <label for="monitorInterval">监控间隔 (秒):</label>
                             <input type="number" id="monitorInterval" name="monitorInterval" min="1" value="60" class="form-control">
                         </div>
                         <div class="button-group">
+                            <button onclick="testNotification()" class="btn btn-secondary">测试通知</button>
                             <button onclick="saveMonitoringConfig()" class="btn btn-primary">保存配置</button>
                             <button onclick="toggleMonitoring()" class="btn btn-secondary" id="toggleMonitoringBtn">开启/关闭监控</button>
                         </div>
@@ -1920,10 +1940,17 @@
             loadContainers();
             loadMonitoringConfig();
             
-            // 绑定保存按钮的点击事件
-            document.querySelector('#docker-monitoring button:nth-of-type(1)').addEventListener('click', saveMonitoringConfig);
+            // 绑定测试通知按钮的点击事件
+            document.querySelector('#docker-monitoring button:nth-of-type(1)').addEventListener('click', testNotification);
+            
+            // 绑定保存配置按钮的点击事件
+            document.querySelector('#docker-monitoring button:nth-of-type(2)').addEventListener('click', saveMonitoringConfig);
+            
             // 绑定开启/关闭监控按钮的点击事件
-            document.querySelector('#docker-monitoring button:nth-of-type(2)').addEventListener('click', toggleMonitoring);
+            document.querySelector('#docker-monitoring button:nth-of-type(3)').addEventListener('click', toggleMonitoring);
+
+            // 为通知类型下拉框添加变更事件监听器
+            document.getElementById('notificationType').addEventListener('change', toggleNotificationFields);
 
             refreshStoppedContainers(); // 初始加载已停止的容器
 
@@ -2072,17 +2099,70 @@
                 }
             }
 
+            function toggleNotificationFields() {
+                const notificationType = document.getElementById('notificationType').value;
+                const wechatFields = document.getElementById('wechatFields');
+                const telegramFields = document.getElementById('telegramFields');
+
+                if (notificationType === 'wechat') {
+                    wechatFields.style.display = 'block';
+                    telegramFields.style.display = 'none';
+                } else if (notificationType === 'telegram') {
+                    wechatFields.style.display = 'none';
+                    telegramFields.style.display = 'block';
+                }
+            }
+
+            async function testNotification() {
+                const notificationType = document.getElementById('notificationType').value;
+                let data = {
+                    notificationType: notificationType,
+                    monitorInterval: document.getElementById('monitorInterval').value
+                };
+
+                if (notificationType === 'wechat') {
+                    data.webhookUrl = document.getElementById('webhookUrl').value;
+                } else if (notificationType === 'telegram') {
+                    data.telegramToken = document.getElementById('telegramToken').value;
+                    data.telegramChatId = document.getElementById('telegramChatId').value;
+                }
+
+                try {
+                    const response = await fetch('/api/test-notification', {
+                        method: 'POST',
+                        headers: { 'Content-Type': 'application/json' },
+                        body: JSON.stringify(data)
+                    });
+
+                    if (response.ok) {
+                        const result = await response.json();
+                        showMessage(result.message || '通知测试成功!');
+                    } else {
+                        const errorData = await response.json();
+                        throw new Error(errorData.error || '测试失败');
+                    }
+                } catch (error) {
+                    showMessage('通知测试失败: ' + error.message, true);
+                }
+            }
+
             // 修改保存配置的函数
             async function saveMonitoringConfig() {
-                const webhookUrl = document.getElementById('webhookUrl').value;
-                const monitorInterval = document.getElementById('monitorInterval').value;
-                const isEnabled = document.getElementById('monitoringStatus').textContent === '已开启';
+                const notificationType = document.getElementById('notificationType').value;
+                let data = {
+                    notificationType: notificationType,
+                    monitorInterval: document.getElementById('monitorInterval').value,
+                    isEnabled: document.getElementById('monitoringStatus').textContent === '已开启',
+                    webhookUrl: document.getElementById('webhookUrl').value,
+                    telegramToken: document.getElementById('telegramToken').value,
+                    telegramChatId: document.getElementById('telegramChatId').value
+                };
 
                 try {
                     const response = await fetch('/api/monitoring-config', {
                         method: 'POST',
                         headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ webhookUrl, monitorInterval, isEnabled })
+                        body: JSON.stringify(data)
                     });
                     if (response.ok) {
                         showMessage('监控配置已保存');
@@ -2125,9 +2205,15 @@
                 try {
                     const response = await fetch('/api/monitoring-config');
                     const config = await response.json();
+                    
+                    document.getElementById('notificationType').value = config.notificationType || 'wechat';
                     document.getElementById('webhookUrl').value = config.webhookUrl || '';
+                    document.getElementById('telegramToken').value = config.telegramToken || '';
+                    document.getElementById('telegramChatId').value = config.telegramChatId || '';
                     document.getElementById('monitorInterval').value = config.monitorInterval || 60;
+                    
                     updateMonitoringStatus(config.isEnabled);
+                    toggleNotificationFields(); // 确保根据加载的配置显示正确的字段
                 } catch (error) {
                     showMessage('加载监控配置失败: ' + error.message, true);
                 }
@@ -2191,7 +2277,8 @@
 
             // 确保在页面加载时初始化停止的容器列表
             document.addEventListener('DOMContentLoaded', () => {
-                refreshStoppedContainers();
+                loadMonitoringConfig();
+                document.getElementById('notificationType').addEventListener('change', toggleNotificationFields);
             });
 
             // 页面加载时初始化

部分文件因为文件数量过多而无法显示