From edbaa9a25715e145bda663a9310b04b7985c508c Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Thu, 23 Apr 2026 23:33:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=82=AE=E4=BB=B6=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E8=A7=84=E5=88=99=E5=A4=9A=E7=A7=8D=E8=A7=A6=E5=8F=91=E6=9C=BA?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 159 +++++++++++++++++++++++++++++++++++++++++++++++++-- logs/app.log | 40 ++++++------- 2 files changed, 171 insertions(+), 28 deletions(-) diff --git a/app.py b/app.py index 27daa89..0e0df17 100644 --- a/app.py +++ b/app.py @@ -1103,7 +1103,7 @@ HTML_TEMPLATE = ''' - 项目服务管理面板 v3.1 + 项目服务管理面板 v3.2 @@ -1738,6 +1738,25 @@ HTML_TEMPLATE = ''' +
+ + +
+ + +
@@ -2407,15 +2426,24 @@ HTML_TEMPLATE = ''' } const resourceNames = { 'cpu': 'CPU', 'memory': '内存', 'disk': '磁盘' }; + const triggerNames = { + 'single': '一次性', + 'continuous': `连续${rule.triggerParam}次`, + 'duration': `持续${rule.triggerParam}秒`, + 'average': `平均${rule.triggerParam}次` + }; list.innerHTML = rules.map(rule => { const silentInfo = rule.silentEnabled ? `静默:${rule.silentStart}-${rule.silentEnd}` : ''; + const triggerType = rule.triggerType || 'single'; + const triggerDisplay = triggerNames[triggerType] || triggerNames['single']; return `
${rule.enabled ? '启用' : '禁用'} ${rule.name} ${resourceNames[rule.resource]} ≥ ${rule.threshold}% + ${triggerDisplay} → ${rule.email} ${silentInfo ? ` ${silentInfo}` : ''}
@@ -2434,6 +2462,9 @@ HTML_TEMPLATE = ''' document.getElementById('emailRuleName').value = ''; document.getElementById('emailRuleResource').value = 'cpu'; document.getElementById('emailRuleThreshold').value = 80; + document.getElementById('emailRuleTriggerType').value = 'single'; + document.getElementById('emailRuleTriggerParam').value = 3; + document.getElementById('triggerParamsDiv').style.display = 'none'; document.getElementById('emailRuleAddress').value = ''; document.getElementById('emailRuleInterval').value = 300; document.getElementById('emailRuleSilentStart').value = '23:00'; @@ -2447,6 +2478,46 @@ HTML_TEMPLATE = ''' document.getElementById('emailRuleModal').classList.add('hidden'); } + function updateTriggerParams() { + const triggerType = document.getElementById('emailRuleTriggerType').value; + const paramsDiv = document.getElementById('triggerParamsDiv'); + const labelEl = document.getElementById('triggerParamsLabel'); + const unitEl = document.getElementById('triggerParamUnit'); + const hintEl = document.getElementById('triggerParamHint'); + const paramInput = document.getElementById('emailRuleTriggerParam'); + + // 根据触发类型设置不同的参数 + switch (triggerType) { + case 'single': + paramsDiv.style.display = 'none'; + break; + case 'continuous': + paramsDiv.style.display = 'flex'; + labelEl.textContent = '连续次数'; + unitEl.textContent = '次'; + hintEl.textContent = '连续超过阈值多少次才触发'; + paramInput.max = 100; + paramInput.value = 3; + break; + case 'duration': + paramsDiv.style.display = 'flex'; + labelEl.textContent = '持续时间'; + unitEl.textContent = '秒'; + hintEl.textContent = '累计超过阈值多少秒才触发'; + paramInput.max = 3600; + paramInput.value = 60; + break; + case 'average': + paramsDiv.style.display = 'flex'; + labelEl.textContent = '采样次数'; + unitEl.textContent = '次'; + hintEl.textContent = '最近N次采样的平均值超过阈值才触发'; + paramInput.max = 100; + paramInput.value = 5; + break; + } + } + function editEmailRule(ruleId) { const rules = getEmailRules(); const rule = rules.find(r => r.id === ruleId); @@ -2457,6 +2528,9 @@ HTML_TEMPLATE = ''' document.getElementById('emailRuleName').value = rule.name; document.getElementById('emailRuleResource').value = rule.resource; document.getElementById('emailRuleThreshold').value = rule.threshold; + document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single'; + document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3; + updateTriggerParams(); document.getElementById('emailRuleAddress').value = rule.email; document.getElementById('emailRuleInterval').value = rule.interval; document.getElementById('emailRuleSilentStart').value = rule.silentStart || '23:00'; @@ -2473,6 +2547,8 @@ HTML_TEMPLATE = ''' const name = document.getElementById('emailRuleName').value.trim(); const resource = document.getElementById('emailRuleResource').value; const threshold = parseInt(document.getElementById('emailRuleThreshold').value); + const triggerType = document.getElementById('emailRuleTriggerType').value; + const triggerParam = parseInt(document.getElementById('emailRuleTriggerParam').value) || 3; const email = document.getElementById('emailRuleAddress').value.trim(); const interval = parseInt(document.getElementById('emailRuleInterval').value) || 300; const silentStart = document.getElementById('emailRuleSilentStart').value; @@ -2491,14 +2567,17 @@ HTML_TEMPLATE = ''' // 编辑 const idx = rules.findIndex(r => r.id === ruleId); if (idx >= 0) { - rules[idx] = { ...rules[idx], name, resource, threshold, email, interval, silentStart, silentEnd, silentEnabled, enabled }; + rules[idx] = { ...rules[idx], name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled }; } } else { // 新增 rules.push({ id: 'rule_' + Date.now(), - name, resource, threshold, email, interval, silentStart, silentEnd, silentEnabled, enabled, - lastSent: 0 + name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled, + lastSent: 0, + exceedCount: 0, // 连续超过次数 + exceedDuration: 0, // 累计超过时间 + recentValues: [] // 最近采样值 }); } @@ -2548,9 +2627,77 @@ HTML_TEMPLATE = ''' } } - // 检查阈值 + // 检查阈值和触发机制 const value = data[rule.resource]?.percent; - if (value === undefined || value < rule.threshold) continue; + if (value === undefined) continue; + + // 初始化触发相关数据 + if (!rule.exceedCount) rule.exceedCount = 0; + if (!rule.exceedDuration) rule.exceedDuration = 0; + if (!rule.recentValues) rule.recentValues = []; + if (!rule.triggerType) rule.triggerType = 'single'; + if (!rule.triggerParam) rule.triggerParam = 3; + + const triggerType = rule.triggerType; + const triggerParam = rule.triggerParam; + const isExceedThreshold = value >= rule.threshold; + + let shouldTrigger = false; + + // 根据触发类型判断 + switch (triggerType) { + case 'single': + // 一次性触发:超过阈值立即触发 + if (isExceedThreshold) shouldTrigger = true; + break; + + case 'continuous': + // 连续触发:连续超过阈值N次 + if (isExceedThreshold) { + rule.exceedCount++; + if (rule.exceedCount >= triggerParam) { + shouldTrigger = true; + rule.exceedCount = 0; // 触发后重置计数 + } + } else { + rule.exceedCount = 0; // 未超过阈值时重置计数 + } + break; + + case 'duration': + // 持续时间触发:累计超过阈值N秒 + if (isExceedThreshold) { + // 增加累计时间(假设实时监控间隔为2秒) + const intervalSeconds = realtimeInterval || 2; + rule.exceedDuration += intervalSeconds; + if (rule.exceedDuration >= triggerParam) { + shouldTrigger = true; + rule.exceedDuration = 0; // 触发后重置 + } + } else { + rule.exceedDuration = 0; // 未超过阈值时重置累计时间 + } + break; + + case 'average': + // 平均值触发:最近N次平均值超过阈值 + rule.recentValues.push(value); + if (rule.recentValues.length > triggerParam) { + rule.recentValues.shift(); // 保持最近N个值 + } + if (rule.recentValues.length >= triggerParam) { + const avg = rule.recentValues.reduce((a, b) => a + b, 0) / rule.recentValues.length; + if (avg >= rule.threshold) { + shouldTrigger = true; + } + } + break; + } + + // 保存更新的规则数据 + saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r)); + + if (!shouldTrigger) continue; // 发送邮件 try { diff --git a/logs/app.log b/logs/app.log index 8f1e1de..f038531 100644 --- a/logs/app.log +++ b/logs/app.log @@ -1,8 +1,8 @@ -[2026-04-23 23:25:57] ================================================== -[2026-04-23 23:25:57] 项目服务管理面板 v2.0.0 启动 -[2026-04-23 23:25:57] 访问地址: http://localhost:19013 -[2026-04-23 23:25:57] 进程PID: 1262750 -[2026-04-23 23:25:57] ================================================== +[2026-04-23 23:33:00] ================================================== +[2026-04-23 23:33:00] 项目服务管理面板 v2.0.0 启动 +[2026-04-23 23:33:00] 访问地址: http://localhost:19013 +[2026-04-23 23:33:00] 进程PID: 1265636 +[2026-04-23 23:33:00] ================================================== * Serving Flask app 'app' * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. @@ -10,20 +10,16 @@ WARNING: This is a development server. Do not use it in a production deployment. * Running on http://127.0.0.1:19013 * Running on http://192.168.2.17:19013 Press CTRL+C to quit -127.0.0.1 - - [23/Apr/2026 23:26:02] "GET / HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:03] "GET / HTTP/1.1" 200 - -192.168.2.14 - - [23/Apr/2026 23:26:04] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:06] "GET / HTTP/1.1" 200 - -192.168.2.8 - - [23/Apr/2026 23:26:08] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:13] "GET / HTTP/1.1" 200 - -192.168.2.14 - - [23/Apr/2026 23:26:14] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:16] "GET / HTTP/1.1" 200 - -192.168.2.8 - - [23/Apr/2026 23:26:18] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:23] "GET / HTTP/1.1" 200 - -192.168.2.14 - - [23/Apr/2026 23:26:24] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:26] "GET / HTTP/1.1" 200 - -192.168.2.8 - - [23/Apr/2026 23:26:28] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:28] "GET / HTTP/1.1" 200 - -192.168.2.8 - - [23/Apr/2026 23:26:30] "GET /api/projects HTTP/1.1" 200 - -127.0.0.1 - - [23/Apr/2026 23:26:33] "GET / HTTP/1.1" 200 - -192.168.2.14 - - [23/Apr/2026 23:26:34] "GET /api/projects HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:03] "GET / HTTP/1.1" 200 - +192.168.2.8 - - [23/Apr/2026 23:33:05] "GET /api/projects HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:06] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:13] "GET / HTTP/1.1" 200 - +192.168.2.8 - - [23/Apr/2026 23:33:15] "GET /api/projects HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:23] "GET / HTTP/1.1" 200 - +192.168.2.8 - - [23/Apr/2026 23:33:25] "GET /api/projects HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:31] "GET / HTTP/1.1" 200 - +192.168.2.8 - - [23/Apr/2026 23:33:33] "GET /api/projects HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:33] "GET / HTTP/1.1" 200 - +127.0.0.1 - - [23/Apr/2026 23:33:33] "GET / HTTP/1.1" 200 - +192.168.2.14 - - [23/Apr/2026 23:33:35] "GET /api/projects HTTP/1.1" 200 - +192.168.2.8 - - [23/Apr/2026 23:33:35] "GET /api/projects HTTP/1.1" 200 -