feat(notify): 告警补服务端掉线/Agent 不可达/配置漂移,巡检改 Source 接口(v1.2.10)
CI / verify (push) Canceled after 0s

- 三个新事件(默认关闭):公网服务端掉线、公网 agent 不可达、配置漂移;连续失败阈值与漂移提醒间隔均可配,达标与恢复各报一次

- 避免误报:只在「用户要服务端运行」时判定掉线;ConnectionState=unknown 按健康处理;没配 agent 的服务端不参与可达性判定

- internal/monitor 改 Source 接口(Routes/Servers/Agents/Drift),frp 适配在 internal/app/monitor_source.go;「连续 N 次」抽成共用 failureTransition,通知一律在锁外调用

- 通知图标分类补齐;通知方式标签改「Webhook / IM 机器人」;新增 6 个设置项;补 serverDown/serverLabel/stateOr 测试

- agent 无改动,AgentVersion 保持 0.12.2
This commit is contained in:
2026-09-25 09:54:48 +08:00
parent 862442e77f
commit c8264bf280
17 changed files with 606 additions and 70 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "fonu-web",
"private": true,
"version": "1.2.9",
"version": "1.2.10",
"packageManager": "npm@10.9.4",
"type": "module",
"scripts": {
+25 -1
View File
@@ -3,7 +3,7 @@ export interface NotifyEventOption {
label: string
hint?: string
hasThreshold?: boolean
thresholdKey?: 'ip_frequent' | 'login_failure' | 'resource' | 'tunnel_fail'
thresholdKey?: 'ip_frequent' | 'login_failure' | 'resource' | 'tunnel_fail' | 'frps_fail' | 'agent_fail' | 'drift_cooldown'
}
export interface NotifyEventGroup {
@@ -85,6 +85,27 @@ export const NOTIFY_EVENT_GROUPS: NotifyEventGroup[] = [
hasThreshold: true,
thresholdKey: 'tunnel_fail',
},
{
key: 'on_frps_down',
label: '公网服务端掉线',
hint: '服务端进程没在跑或隧道连不上 frps(手动停止的不报),恢复时再通知一次',
hasThreshold: true,
thresholdKey: 'frps_fail',
},
{
key: 'on_agent_unreachable',
label: '公网 agent 不可达',
hint: 'VPS 上的 fonu-agent 连续无响应(重启后没起来最常见),恢复时再通知一次',
hasThreshold: true,
thresholdKey: 'agent_fail',
},
{
key: 'on_route_drift',
label: '配置漂移',
hint: 'VPS 上的 vhost 与 Fonu 记录不一致(被手工改过或重装后未重部署),同一台按提醒间隔重复提醒',
hasThreshold: true,
thresholdKey: 'drift_cooldown',
},
],
},
]
@@ -97,3 +118,6 @@ export const DEFAULT_CPU_THRESHOLD = 90
export const DEFAULT_MEMORY_THRESHOLD = 90
export const DEFAULT_DISK_THRESHOLD = 85
export const DEFAULT_TUNNEL_FAIL_THRESHOLD = 3
export const DEFAULT_FRPS_FAIL_THRESHOLD = 2
export const DEFAULT_AGENT_FAIL_THRESHOLD = 3
export const DEFAULT_DRIFT_COOLDOWN_HOURS = 24
+2 -1
View File
@@ -3,7 +3,8 @@ import type { NotifyType, WebhookProvider } from '../api/types'
export const NOTIFY_TYPE_OPTIONS: { label: string; value: NotifyType }[] = [
{ label: '关闭', value: '' },
{ label: '邮件(SMTP)', value: 'email' },
{ label: 'Webhook', value: 'webhook' },
// 企业微信 / 钉钉 / 飞书机器人都是「POST 一个 URL + JSON」,归在 Webhook 预设里,标签要说明白
{ label: 'Webhook / IM 机器人', value: 'webhook' },
{ label: 'Telegram 机器人', value: 'telegram' },
]
+42
View File
@@ -334,6 +334,24 @@
<n-input-number v-model:value="notifyTunnelFailThreshold" :min="1" :max="20" class="settings-field__input" />
</div>
</template>
<template v-else-if="event.thresholdKey === 'frps_fail'">
<div class="settings-field">
<label class="settings-field__label">连续失败次数</label>
<n-input-number v-model:value="notifyFrpsFailThreshold" :min="1" :max="20" class="settings-field__input" />
</div>
</template>
<template v-else-if="event.thresholdKey === 'agent_fail'">
<div class="settings-field">
<label class="settings-field__label">连续失败次数</label>
<n-input-number v-model:value="notifyAgentFailThreshold" :min="1" :max="20" class="settings-field__input" />
</div>
</template>
<template v-else-if="event.thresholdKey === 'drift_cooldown'">
<div class="settings-field">
<label class="settings-field__label">提醒间隔(小时)</label>
<n-input-number v-model:value="notifyDriftCooldownHours" :min="1" :max="168" class="settings-field__input" />
</div>
</template>
</div>
</template>
</div>
@@ -716,8 +734,11 @@ import PageHeader from '../components/PageHeader.vue'
import StatusBadge from '../components/StatusBadge.vue'
import { DEFAULT_PRIVATE_CIDRS, mergeIPList, stripDefaultIPList } from '../constants/ipPolicy'
import {
DEFAULT_AGENT_FAIL_THRESHOLD,
DEFAULT_CPU_THRESHOLD,
DEFAULT_DISK_THRESHOLD,
DEFAULT_DRIFT_COOLDOWN_HOURS,
DEFAULT_FRPS_FAIL_THRESHOLD,
DEFAULT_LOGIN_FAILURE_THRESHOLD,
DEFAULT_LOGIN_FAILURE_WINDOW_SEC,
DEFAULT_MEMORY_THRESHOLD,
@@ -767,6 +788,9 @@ const notifyEventFlags = reactive<Record<string, boolean>>({
on_nginx_reload_failure: true,
on_resource_threshold: false,
on_tunnel_down: false,
on_frps_down: false,
on_agent_unreachable: false,
on_route_drift: false,
})
const notifyIPFrequentThreshold = ref(100)
const notifyIPFrequentWindowSec = ref(60)
@@ -776,6 +800,9 @@ const notifyCPUThreshold = ref(DEFAULT_CPU_THRESHOLD)
const notifyMemoryThreshold = ref(DEFAULT_MEMORY_THRESHOLD)
const notifyDiskThreshold = ref(DEFAULT_DISK_THRESHOLD)
const notifyTunnelFailThreshold = ref(DEFAULT_TUNNEL_FAIL_THRESHOLD)
const notifyFrpsFailThreshold = ref(DEFAULT_FRPS_FAIL_THRESHOLD)
const notifyAgentFailThreshold = ref(DEFAULT_AGENT_FAIL_THRESHOLD)
const notifyDriftCooldownHours = ref(DEFAULT_DRIFT_COOLDOWN_HOURS)
// API Token:明文只在创建响应里回一次,因此单独用一个 ref 暂存展示
const apiTokens = ref<ApiToken[]>([])
@@ -1278,6 +1305,9 @@ function applyNotifySettings(settings: Record<string, string>) {
notifyEventFlags.on_nginx_reload_failure = flagOn('notify_on_nginx_reload_failure')
notifyEventFlags.on_resource_threshold = flagOptIn('notify_on_resource_threshold')
notifyEventFlags.on_tunnel_down = flagOptIn('notify_on_tunnel_down')
notifyEventFlags.on_frps_down = flagOptIn('notify_on_frps_down')
notifyEventFlags.on_agent_unreachable = flagOptIn('notify_on_agent_unreachable')
notifyEventFlags.on_route_drift = flagOptIn('notify_on_route_drift')
} else {
const legacyDDNS = flagOn('notify_on_ddns_error')
const legacyCert = flagOn('notify_on_cert_error')
@@ -1291,6 +1321,9 @@ function applyNotifySettings(settings: Record<string, string>) {
notifyEventFlags.on_login_failure = false
notifyEventFlags.on_resource_threshold = false
notifyEventFlags.on_tunnel_down = false
notifyEventFlags.on_frps_down = false
notifyEventFlags.on_agent_unreachable = false
notifyEventFlags.on_route_drift = false
}
notifyIPFrequentThreshold.value = Number(settings.notify_ip_frequent_threshold || 100)
notifyIPFrequentWindowSec.value = Number(settings.notify_ip_frequent_window_sec || 60)
@@ -1300,6 +1333,9 @@ function applyNotifySettings(settings: Record<string, string>) {
notifyMemoryThreshold.value = Number(settings.notify_memory_threshold || DEFAULT_MEMORY_THRESHOLD)
notifyDiskThreshold.value = Number(settings.notify_disk_threshold || DEFAULT_DISK_THRESHOLD)
notifyTunnelFailThreshold.value = Number(settings.notify_tunnel_fail_threshold || DEFAULT_TUNNEL_FAIL_THRESHOLD)
notifyFrpsFailThreshold.value = Number(settings.notify_frps_fail_threshold || DEFAULT_FRPS_FAIL_THRESHOLD)
notifyAgentFailThreshold.value = Number(settings.notify_agent_fail_threshold || DEFAULT_AGENT_FAIL_THRESHOLD)
notifyDriftCooldownHours.value = Number(settings.notify_drift_cooldown_hours || DEFAULT_DRIFT_COOLDOWN_HOURS)
}
function buildNotifyEmailJSON(): string {
@@ -1407,6 +1443,12 @@ function buildSavePayload() {
notify_on_nginx_reload_failure: notifyEventFlags.on_nginx_reload_failure ? '1' : '0',
notify_on_resource_threshold: notifyEventFlags.on_resource_threshold ? '1' : '0',
notify_on_tunnel_down: notifyEventFlags.on_tunnel_down ? '1' : '0',
notify_on_frps_down: notifyEventFlags.on_frps_down ? '1' : '0',
notify_on_agent_unreachable: notifyEventFlags.on_agent_unreachable ? '1' : '0',
notify_on_route_drift: notifyEventFlags.on_route_drift ? '1' : '0',
notify_frps_fail_threshold: String(notifyFrpsFailThreshold.value || DEFAULT_FRPS_FAIL_THRESHOLD),
notify_agent_fail_threshold: String(notifyAgentFailThreshold.value || DEFAULT_AGENT_FAIL_THRESHOLD),
notify_drift_cooldown_hours: String(notifyDriftCooldownHours.value || DEFAULT_DRIFT_COOLDOWN_HOURS),
notify_cpu_threshold: String(notifyCPUThreshold.value || DEFAULT_CPU_THRESHOLD),
notify_memory_threshold: String(notifyMemoryThreshold.value || DEFAULT_MEMORY_THRESHOLD),
notify_disk_threshold: String(notifyDiskThreshold.value || DEFAULT_DISK_THRESHOLD),