feat: 配置版本历史/对比/回滚、国内 IM 通知渠道、阈值告警与 API Token(v1.2.9)
CI / verify (push) Canceled after 0s

- 配置版本:fsutil 版本化备份(内容相同自动去重),主 nginx.conf 与 VPS vhost 的版本列表/逐行差异/一键回滚,回滚先校验语法与 nginx -t 再写回

- agent 0.12.2:写 vhost 前留版本;nginx -t 校验失败改为恢复上一个可用版本(此前直接删文件),新增域名 vhost 版本端点,双架构二进制已重编

- 通知:补企业微信 / 钉钉 / 飞书机器人,切换预设时服务地址跟随该预设默认值

- 阈值告警:新增 internal/monitor 巡检(CPU/内存/磁盘持续超阈值、隧道连续 N 次未注册),默认关闭,带静默期与恢复通知

- API Token:api_tokens 表 + Authorization Bearer 认证,只读/读写两种范围;令牌管理与改密/退出仅限登录会话
This commit is contained in:
2026-09-24 20:50:17 +08:00
parent 8f75e99339
commit 862442e77f
42 changed files with 3034 additions and 190 deletions
+71
View File
@@ -0,0 +1,71 @@
package monitor
import "testing"
func TestTunnelTransitionAlertsOncePerOutage(t *testing.T) {
const limit = 3
failures := 0
var events []tunnelEvent
// 连续三轮不通:只在第 3 轮(刚好达到阈值)发一次
for i := 0; i < 5; i++ {
var event tunnelEvent
failures, event = tunnelTransition(failures, false, limit)
if event != tunnelEventNone {
events = append(events, event)
}
}
if len(events) != 1 || events[0] != tunnelEventDown {
t.Fatalf("连续不通应只告警一次,实际 %v", events)
}
if failures != 5 {
t.Fatalf("失败计数应继续累加,实际 %d", failures)
}
// 恢复:发一次恢复通知并清零
failures, event := tunnelTransition(failures, true, limit)
if event != tunnelEventRecovered || failures != 0 {
t.Fatalf("恢复应通知一次并清零:event=%v failures=%d", event, failures)
}
// 恢复后再次不通:还能再报一次
failures, event = tunnelTransition(failures, false, limit)
if event != tunnelEventNone || failures != 1 {
t.Fatalf("恢复后重新计数:event=%v failures=%d", event, failures)
}
failures, event = tunnelTransition(failures, false, limit)
if event != tunnelEventNone {
t.Fatalf("未达阈值不应告警:%v", event)
}
if _, event = tunnelTransition(failures, false, limit); event != tunnelEventDown {
t.Fatalf("第二次中断达到阈值应再告警一次:%v", event)
}
}
func TestTunnelTransitionBelowThresholdIsSilent(t *testing.T) {
// 单次抖动(未达阈值)不打扰用户,也不算作恢复
if failures, event := tunnelTransition(0, true, 3); failures != 0 || event != tunnelEventNone {
t.Fatalf("健康状态不应产生事件:%d %v", failures, event)
}
if failures, event := tunnelTransition(1, true, 3); failures != 0 || event != tunnelEventNone {
t.Fatalf("未达阈值的恢复不应通知:%d %v", failures, event)
}
}
func TestPercent(t *testing.T) {
if got := percent(0, 0); got != 0 {
t.Fatalf("总数为 0 时应返回 0,实际 %v", got)
}
if got := percent(50, 200); got != 25 {
t.Fatalf("期望 25,实际 %v", got)
}
}
func TestDetailOrFallsBack(t *testing.T) {
if got := detailOr(" "); got != "未返回具体原因" {
t.Fatalf("空详情应给出兜底文案,实际 %q", got)
}
if got := detailOr("dashboard 超时"); got != "dashboard 超时" {
t.Fatalf("非空详情应原样返回,实际 %q", got)
}
}