@@ -1498,7 +1561,8 @@
return;
}
- const resourceNames = { 'cpu': 'CPU', 'memory': '内存', 'disk': '磁盘' };
+ const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘' };
+ const resourceUnits = { 'cpu': '%', 'cpu_temp': '°C', 'memory': '%', 'disk': '%' };
list.innerHTML = rules.map(rule => {
const silentInfo = rule.silentEnabled ? `静默:${rule.silentStart}-${rule.silentEnd}` : '';
@@ -1509,12 +1573,16 @@
else if (triggerType === 'duration') triggerDisplay = `持续${triggerParam}秒`;
else if (triggerType === 'average') triggerDisplay = `平均${triggerParam}次`;
+ // 显示资源列表(支持多资源和旧版单资源兼容)
+ const resources = rule.resources || (rule.resource ? [rule.resource] : []);
+ const resourceDisplay = resources.map(r => resourceNames[r] || r).join('+');
+
return `
${rule.enabled ? '启用' : '禁用'}
${rule.name}
- ${resourceNames[rule.resource]} ≥ ${rule.threshold}%
+ ${resourceDisplay} ≥ ${rule.threshold}
${triggerDisplay}
→ ${rule.email}
${silentInfo ? ` ${silentInfo}` : ''}
@@ -1532,8 +1600,10 @@
document.getElementById('emailRuleModalTitle').textContent = '添加邮件通知规则';
document.getElementById('emailRuleId').value = '';
document.getElementById('emailRuleName').value = '';
- document.getElementById('emailRuleResource').value = 'cpu';
+ // 重置资源复选框
+ document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
document.getElementById('emailRuleThreshold').value = 80;
+ document.getElementById('emailRuleThresholdUnit').textContent = '% / °C';
document.getElementById('emailRuleTriggerType').value = 'single';
document.getElementById('emailRuleTriggerParam').value = 3;
document.getElementById('triggerParamsDiv').style.display = 'none';
@@ -1550,6 +1620,33 @@
document.getElementById('emailRuleModal').classList.add('hidden');
}
+ function getSelectedResources() {
+ const checkboxes = document.querySelectorAll('.email-rule-resource:checked');
+ return Array.from(checkboxes).map(cb => cb.value);
+ }
+
+ function onResourceChange() {
+ const selected = getSelectedResources();
+ const unitEl = document.getElementById('emailRuleThresholdUnit');
+ const thresholdEl = document.getElementById('emailRuleThreshold');
+
+ if (selected.length === 0) {
+ unitEl.textContent = '% / °C';
+ } else if (selected.length === 1 && selected[0] === 'cpu_temp') {
+ unitEl.textContent = '°C';
+ if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80;
+ thresholdEl.max = 120;
+ thresholdEl.min = 30;
+ } else {
+ unitEl.textContent = '%';
+ thresholdEl.max = 100;
+ thresholdEl.min = 1;
+ if (selected.includes('cpu_temp')) {
+ unitEl.textContent = '% / °C';
+ }
+ }
+ }
+
function updateTriggerParams() {
const triggerType = document.getElementById('emailRuleTriggerType').value;
const paramsDiv = document.getElementById('triggerParamsDiv');
@@ -1598,7 +1695,15 @@
document.getElementById('emailRuleModalTitle').textContent = '编辑邮件通知规则';
document.getElementById('emailRuleId').value = rule.id;
document.getElementById('emailRuleName').value = rule.name;
- document.getElementById('emailRuleResource').value = rule.resource;
+ // 重置所有复选框
+ document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false);
+ // 勾选已保存的资源(支持新版resources数组和旧版resource字符串兼容)
+ const resources = rule.resources || (rule.resource ? [rule.resource] : []);
+ resources.forEach(r => {
+ const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
+ if (cb) cb.checked = true;
+ });
+ onResourceChange();
document.getElementById('emailRuleThreshold').value = rule.threshold;
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
@@ -1617,7 +1722,7 @@
const ruleId = document.getElementById('emailRuleId').value;
const name = document.getElementById('emailRuleName').value.trim();
- const resource = document.getElementById('emailRuleResource').value;
+ const resources = getSelectedResources();
const threshold = parseInt(document.getElementById('emailRuleThreshold').value);
const triggerType = document.getElementById('emailRuleTriggerType').value;
const triggerParam = parseInt(document.getElementById('emailRuleTriggerParam').value) || 3;
@@ -1633,19 +1738,24 @@
return;
}
+ if (resources.length === 0) {
+ alert('请至少选择一个监控资源');
+ return;
+ }
+
const rules = getEmailRules();
if (ruleId) {
// 编辑
const idx = rules.findIndex(r => r.id === ruleId);
if (idx >= 0) {
- rules[idx] = { ...rules[idx], name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
+ rules[idx] = { ...rules[idx], name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled };
}
} else {
// 新增
rules.push({
id: 'rule_' + Date.now(),
- name, resource, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
+ name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled,
lastSent: 0,
exceedCount: 0, // 连续超过次数
exceedDuration: 0, // 累计超过时间
@@ -1699,77 +1809,90 @@
}
}
- // 检查阈值和触发机制
- const value = data[rule.resource]?.percent;
- if (value === undefined) continue;
+ // 检查阈值和触发机制(支持多资源)
+ const resources = rule.resources || (rule.resource ? [rule.resource] : []);
+ const resourceNames = { 'cpu': 'CPU使用率', 'cpu_temp': 'CPU温度', 'memory': '内存使用率', 'disk': '磁盘使用率' };
- // 初始化触发相关数据
- 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 exceededResources = [];
- 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;
+ for (const resource of resources) {
+ let value;
+ if (resource === 'cpu_temp') {
+ // CPU温度取两个Package中较高的那个
+ const t0 = data.cpu?.temp_package_0;
+ const t1 = data.cpu?.temp_package_1;
+ if (t0 === null && t1 === null) continue;
+ if (t0 === null) value = t1;
+ else if (t1 === null) value = t0;
+ else value = Math.max(t0, t1);
+ } else {
+ value = data[resource]?.percent;
+ }
- case 'continuous':
- // 连续触发:连续超过阈值N次
- if (isExceedThreshold) {
- rule.exceedCount++;
- if (rule.exceedCount >= triggerParam) {
- shouldTrigger = true;
- rule.exceedCount = 0; // 触发后重置计数
- }
- } else {
- rule.exceedCount = 0; // 未超过阈值时重置计数
- }
- break;
+ if (value === undefined || value === null) continue;
- 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;
+ const isExceedThreshold = value >= rule.threshold;
- 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;
+ // 初始化触发相关数据(key后缀避免不同资源冲突)
+ const ck = '_' + resource;
+ if (!rule['exceedCount' + ck]) rule['exceedCount' + ck] = 0;
+ if (!rule['exceedDuration' + ck]) rule['exceedDuration' + ck] = 0;
+ if (!rule['recentValues' + ck]) rule['recentValues' + ck] = [];
+
+ const triggerType = rule.triggerType || 'single';
+ const triggerParam = rule.triggerParam || 3;
+ let resourceTriggered = false;
+
+ switch (triggerType) {
+ case 'single':
+ if (isExceedThreshold) resourceTriggered = true;
+ break;
+ case 'continuous':
+ if (isExceedThreshold) {
+ rule['exceedCount' + ck]++;
+ if (rule['exceedCount' + ck] >= triggerParam) {
+ resourceTriggered = true;
+ rule['exceedCount' + ck] = 0;
+ }
+ } else {
+ rule['exceedCount' + ck] = 0;
}
- }
- break;
+ break;
+ case 'duration':
+ if (isExceedThreshold) {
+ const intervalSec = realtimeInterval || 2;
+ rule['exceedDuration' + ck] += intervalSec;
+ if (rule['exceedDuration' + ck] >= triggerParam) {
+ resourceTriggered = true;
+ rule['exceedDuration' + ck] = 0;
+ }
+ } else {
+ rule['exceedDuration' + ck] = 0;
+ }
+ break;
+ case 'average':
+ rule['recentValues' + ck].push(value);
+ if (rule['recentValues' + ck].length > triggerParam) {
+ rule['recentValues' + ck].shift();
+ }
+ if (rule['recentValues' + ck].length >= triggerParam) {
+ const avg = rule['recentValues' + ck].reduce((a, b) => a + b, 0) / rule['recentValues' + ck].length;
+ if (avg >= rule.threshold) resourceTriggered = true;
+ }
+ break;
+ }
+
+ if (resourceTriggered) {
+ const unit = resource === 'cpu_temp' ? '°C' : '%';
+ exceededResources.push(`${resourceNames[resource]}: ${value.toFixed(1)}${unit} (阈值: ${rule.threshold}${unit})`);
+ }
}
// 保存更新的规则数据
saveEmailRules(getEmailRules().map(r => r.id === rule.id ? rule : r));
- if (!shouldTrigger) continue;
+ if (exceededResources.length === 0) continue;
// 发送邮件
try {
@@ -1779,7 +1902,7 @@
body: JSON.stringify({
to: rule.email,
subject: `[系统告警] ${rule.name}`,
- body: `${rule.name} 触发告警\n\n${rule.resource.toUpperCase()} 使用率: ${value.toFixed(1)}%\n阈值: ${rule.threshold}%\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
+ body: `${rule.name} 触发告警\n\n${exceededResources.join('\n')}\n\n时间: ${new Date().toLocaleString()}\n\n请及时处理。`
})
});
@@ -1794,6 +1917,65 @@
}
}
+ // ==================== 测试邮件发送 ====================
+
+ function showTestEmailModal() {
+ document.getElementById('testEmailAddress').value = '';
+ document.getElementById('testEmailSubject').value = '[测试邮件] 项目服务管理面板';
+ document.getElementById('testEmailBody').value = '这是一封测试邮件,用于验证邮件通知功能是否正常工作。\n\n发送自: 项目服务管理面板\n时间: ' + new Date().toLocaleString();
+ document.getElementById('testEmailResult').classList.add('hidden');
+ document.getElementById('testEmailModal').classList.remove('hidden');
+ }
+
+ function closeTestEmailModal() {
+ document.getElementById('testEmailModal').classList.add('hidden');
+ }
+
+ async function sendTestEmail(event) {
+ event.preventDefault();
+
+ const email = document.getElementById('testEmailAddress').value.trim();
+ const subject = document.getElementById('testEmailSubject').value.trim();
+ const body = document.getElementById('testEmailBody').value.trim();
+ const resultEl = document.getElementById('testEmailResult');
+ const submitBtn = event.target.querySelector('button[type="submit"]');
+
+ if (!email) {
+ alert('请输入收件邮箱');
+ return;
+ }
+
+ submitBtn.disabled = true;
+ submitBtn.innerHTML = ' 发送中...';
+ resultEl.classList.add('hidden');
+
+ try {
+ const res = await fetch('/api/email/send', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ to: email, subject, body })
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ resultEl.className = 'mb-3 p-3 rounded-lg bg-green-500/20 text-green-400 text-sm';
+ resultEl.innerHTML = ` 测试邮件发送成功!
收件人: ${email}`;
+ } else {
+ resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
+ resultEl.innerHTML = ` 发送失败: ${data.error || '未知错误'}`;
+ }
+ resultEl.classList.remove('hidden');
+ } catch (e) {
+ resultEl.className = 'mb-3 p-3 rounded-lg bg-red-500/20 text-red-400 text-sm';
+ resultEl.innerHTML = ` 网络错误: ${e.message}`;
+ resultEl.classList.remove('hidden');
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.innerHTML = ' 发送测试';
+ }
+ }
+
function showAddServiceModal() {
document.getElementById('serviceName').value = '';
document.getElementById('servicePort').value = '';