Files
mrlee 74d731d0ac
CI / verify (push) Canceled after 0s
feat: 自动备份与保留策略、健康持续化与状态页、指标历史与趋势图,并修隧道判定误报与仪表盘布局(v1.2.11)
- 自动备份:每日一份(按最新备份是否在 24h 内判定,重启不重复)、保留份数可配、列表/下载/恢复/删除,恢复前先校验归档;顺手修 writeArchive 逐个检查 tar/gzip/文件 Close()(审查报告 §二)

- 健康持续化:只存状态变更(up/down/unknown)并推导可用率与不可用区间;公开状态页默认关闭、访问码存 SHA-256、按 IP 限流、只暴露域名与可用率

- 隧道判定修复:RouteHealthCandidate 新增 tunnel_known,frps 管理接口不可用时不再把「判不出来」当成「不通」(此前会误报「隧道连续不通」);汇总与徽标同样区分「无法判定」

- 指标历史:metrics_samples 纵向点表(本机 CPU/内存/磁盘 + 每规则上下行,30 天保留、查询按桶平均、不做降采样表);仪表盘趋势卡支持 1h/24h/7d

- 仪表盘布局:运行状态并入顶部统计区(运行时长补进「服务」卡);性能并入系统资源那一行,整行 4 列

- 测试:backup/metrics/frp/api/app 新增用例;gofmt、go build/vet/test、vue-tsc、vite build 全绿

- agent 无改动,AgentVersion 保持 0.12.2
2026-09-25 13:05:58 +08:00

143 lines
3.2 KiB
Go

package notify
import (
"strings"
"time"
)
func formatFooter(at time.Time) string {
return "Fonu · " + at.Format("2006-01-02 15:04:05")
}
type eventMeta struct {
icon string
category string
}
type AlertContent struct {
Event string
Subject string
PushTitle string
PlainBody string
HTMLBody string
Category string
Icon string
Detail string
Timestamp time.Time
}
func FormatAlert(event, title, message string) AlertContent {
return FormatAlertAt(event, title, message, time.Now())
}
func FormatAlertAt(event, title, message string, at time.Time) AlertContent {
meta := eventMetaFor(event)
title = strings.TrimSpace(title)
message = strings.TrimSpace(message)
pushTitle := strings.TrimSpace(meta.icon + " " + title)
subject := "[Fonu] " + pushTitle
footer := formatFooter(at)
detail := message
if detail == "" {
detail = "暂无更多详情"
}
plain := strings.Join([]string{
pushTitle,
strings.Repeat("─", 28),
"",
detail,
"",
strings.Repeat("─", 28),
footer,
}, "\n")
html := formatTelegramHTML(pushTitle, detail, at)
return AlertContent{
Event: event,
Subject: subject,
PushTitle: pushTitle,
PlainBody: plain,
HTMLBody: html,
Category: meta.category,
Icon: meta.icon,
Detail: detail,
Timestamp: at,
}
}
func eventMetaFor(event string) eventMeta {
switch event {
case EventDDNSIPChange:
return eventMeta{"🌐", "DDNS"}
case EventDDNSFailure:
return eventMeta{"⚠️", "DDNS"}
case EventCertExpiry:
return eventMeta{"📅", "证书"}
case EventCertRenewSuccess:
return eventMeta{"✅", "证书"}
case EventCertRenewFailure:
return eventMeta{"❌", "证书"}
case EventIPFrequentAccess:
return eventMeta{"🛡️", "安全"}
case EventLoginFailure:
return eventMeta{"🔐", "安全"}
case EventNginxReloadFailure:
return eventMeta{"⚙️", "系统"}
case EventResourceThreshold:
return eventMeta{"📈", "系统"}
case EventTunnelDown:
return eventMeta{"🔌", "隧道"}
case EventFrpsServerDown:
return eventMeta{"🛑", "隧道"}
case EventAgentUnreachable:
return eventMeta{"📡", "隧道"}
case EventRouteDrift:
return eventMeta{"🧭", "配置"}
case EventBackupFailure:
return eventMeta{"💾", "系统"}
case "test":
return eventMeta{"🔔", "系统"}
default:
return eventMeta{"📣", "告警"}
}
}
func formatTelegramHTML(title, detail string, at time.Time) string {
lines := []string{
"<b>" + escapeHTML(title) + "</b>",
"",
"<pre>" + escapeHTML(detail) + "</pre>",
"",
"<i>" + escapeHTML(formatFooter(at)) + "</i>",
}
return strings.Join(lines, "\n")
}
func escapeHTML(s string) string {
replacer := strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
)
return replacer.Replace(s)
}
func (c AlertContent) webhookPayload() map[string]string {
return map[string]string{
"event": c.Event,
"category": c.Category,
"icon": c.Icon,
"title": strings.TrimPrefix(c.PushTitle, c.Icon+" "),
"message": c.Detail,
"formatted": c.PlainBody,
"time": c.Timestamp.UTC().Format(time.RFC3339),
}
}
func (c AlertContent) barkMessage() string {
return strings.Join([]string{c.Detail, "", "────────", formatFooter(c.Timestamp)}, "\n")
}