diff --git a/src/Home.tsx b/src/Home.tsx
index f04d8e7..59fe15c 100644
--- a/src/Home.tsx
+++ b/src/Home.tsx
@@ -42,12 +42,12 @@ const FEATURES = [
},
]
-const DOING = ["服务器基础信息", "网络延迟", "流量统计"]
+const DOING = ["服务器基础信息", "网络延迟", "流量统计", "掉线、流量与到期通知"]
const NOT_DOING = [
"web terminal",
"远程 SSH",
- "通知与告警",
+ "负载告警",
"插件系统",
"ICMP / HTTP 探测",
"agent 自动更新",
diff --git a/src/content/config/notify.mdx b/src/content/config/notify.mdx
new file mode 100644
index 0000000..7ee87fc
--- /dev/null
+++ b/src/content/config/notify.mdx
@@ -0,0 +1,240 @@
+hub 可以把节点掉线、流量快用完、快到期这几件事推给你。渠道有两个:Telegram 机器人,以及一个可以
+自定义请求体的 Webhook。两个都配上就两边都发。
+
+全部在 hub 上完成,agent 不用升级。
+
+## 通知哪些事
+
+| 事件 | 什么时候发 | 在哪开关 |
+|---|---|---|
+| 🔴 离线 | 节点断开超过宽限期(默认 3 分钟) | **每个节点单独开**,默认关 |
+| 🟢 恢复在线 | 发过离线通知的节点重新连上 | 跟着离线走 |
+| ⚠️ 流量提醒 | 本期用量达到阈值(默认 80%)和 100% 时各一次 | 节点填了每月流量额度就生效 |
+| ⏳ 即将到期 | 每天 9 点汇总一条,列出 7 天内到期的节点 | 节点填了到期日就生效 |
+| 🔁 已自动续期 | 在线节点过了到期日,hub 把日期往后顺延时 | 同上 |
+| 🔑 面板登录 | 应急密码或 GitHub 登录成功 | 全局开关,默认开 |
+
+几条规则:
+
+- **宽限期内断开又连上的,什么都不发。** 恢复通知只跟在离线通知后面,网络抖一下不会刷屏。
+- **反复掉线的节点会自动安静下来。** 节点在最近 1 小时内有过一次超过宽限期的掉线,下一次掉线要持续
+ **30 分钟**才报;稳定在线满 1 小时后恢复正常宽限期。真掉线仍然会报,只是最多晚 30 分钟。模拟一台
+ 「在线 1 分钟、掉 4 分钟」来回反复的机器,一小时从 23 条降到 2 条。几十秒的断开(比如重启 agent)不算。
+- **同时发生的事合并发。** hub 每 30 秒检查一次,同一轮里新离线的节点合成一条、恢复的合成一条、
+ 流量超标的合成一条。hub 自己断网时,40 台节点在模拟里是 2 条「N 台节点离线」,不是 40 条。
+- **重启 hub 不会重发。** 已经报过离线的节点记在数据库里,hub 重启后不会再报一遍,节点回来时照样发恢复。
+ hub 停机期间掉线的节点,hub 起来后过了宽限期也会补报。
+- **流量按节点的计费方式算**(上下行相加、取较大值、仅上行、仅下行),周期按重置日算,和面板显示的
+ 是同一个数。流量换了周期会重新计。
+- **只报登录成功,不报失败。** 失败尝试谁都能发起,报失败等于让别人往你的 Telegram 里刷屏。失败有
+ 登录限流管着,见[登录与安全](/config/auth)。
+- **一条消息最多列 20 台节点**,其余写成「……另外 N 台」。Discord 一条消息上限 2000 字符、企业微信
+ 2048 字节,超了整条会被拒收。
+- **每条通知最多送一次。** 渠道暂时不通时会重试,重试完仍失败就丢弃,不会等渠道恢复后补发。
+- **不做 CPU、内存、磁盘这类负载告警。**
+
+
+ **hub 自己停机或断网时,什么通知都发不出来。** 探针没法报告自己的死亡。要盯住 hub 本身,用一个外部
+ 拨测服务(UptimeRobot、Uptime Kuma 之类)定时访问 hub 的地址。
+
+
+
+ 「每天 9 点」和消息里的时间都按 hub 所在机器的时区算。Docker 部署记得设 `TZ`,否则是 UTC 的 9 点,
+ 见 [Docker 部署](/install/docker)。
+
+
+## 配置
+
+后台左侧的**通知**页。填好渠道并保存,再点右上角**发送测试**(测试用的是已保存的配置),每个渠道的报错
+会原样显示出来。企业微信、钉钉、飞书是例外,见下面「[常见服务的写法](#常见服务的写法)」。
+
+然后在同一页的**离线通知**卡片里打开要盯的节点,可以全部打开;单台节点的编辑弹窗里也有这个开关。
+流量和到期提醒不用单独开。
+
+事件卡片里三个数字:
+
+| 设置 | 默认 | 范围 |
+|---|---|---|
+| 离线宽限期 | 3 分钟 | 1–1440 |
+| 流量提醒 | 80% | 0–100,填 0 关闭 |
+| 到期提醒 | 7 天 | 0–365,填 0 同时关闭到期和续期通知 |
+
+
+ 宽限期从 hub **发现连接断开**时算起,hub 每 30 秒检查一次,所以宽限期 3 分钟时,通知在断开后
+ 3 到 3.5 分钟之间到达,和 agent 的上报间隔无关。agent 进程退出、机器正常重启时连接会立刻关闭;
+ 机器直接断电、断网时连接不会正常关闭,hub 最长要 150 秒才发现,通知相应更晚。
+
+
+## Telegram
+
+1. 找 [@BotFather](https://t.me/BotFather) 发 `/newbot`,按提示起名,拿到形如 `123456:ABC-DEF…`
+ 的 token。
+2. 拿 Chat ID:
+ - **发给自己**:先给你的机器人随便发一句话,再打开
+ `https://api.telegram.org/bot/getUpdates`,找 `"chat":{"id":…}` 里的数字。
+ - **发到群组**:把机器人拉进群,在群里发一句话,同样看 `getUpdates`。群组的 ID 是负数,通常以 `-100` 开头。
+ - **发到公开频道**:把机器人设为频道管理员,Chat ID 填 `@频道名`。
+3. 两项填进面板,保存,发送测试。
+
+消息是纯文本,默认第一行是标题,下面是正文:
+
+```text
+🔴 香港 · 甲商家 离线
+最后上报 09-15 20:13 +08:00
+```
+
+想改格式就改**消息模板**,占位符见下面「[自定义内容](#自定义内容)」。
+
+hub 要能访问 `api.telegram.org`。访问不了时给 hub 进程设 `HTTPS_PROXY` 环境变量,它会让 hub 的**所有**
+出站请求都走这个代理(包括转发 agent 二进制、GitHub 登录、查国家码):
+
+```bash
+systemctl edit monitor-hub # 加两行后保存:[Service] 和 Environment=HTTPS_PROXY=http://127.0.0.1:7890
+systemctl restart monitor-hub
+```
+
+Docker 部署在 `docker run` 里加 `-e HTTPS_PROXY=http://…`。
+
+## 自定义内容
+
+Telegram 有一个**消息模板**,Webhook 有一个**请求体模板**,两个渠道各一个,所有事件共用。每类事件的
+标题和正文 hub 已经写好,模板只决定怎么排、加什么前后缀。填写时输入框下面会用一条离线通知实时预览。
+
+| 占位符 | 内容 |
+|---|---|
+| `{{title}}` | 标题,如 `🔴 香港 · 甲商家 离线`、`⚠️ 香港 · 甲商家 流量提醒` |
+| `{{message}}` | 正文,如 `最后上报 09-15 20:13 +08:00`;多台节点时一行一台 |
+| `{{node}}` | 涉及的节点名,多台用 `, ` 隔开;登录和测试为空 |
+| `{{event}}` | `offline` `online` `traffic` `expiry` `renew` `login` `test` 之一 |
+| `{{site}}` | 设置页里的站点名称,默认 `Monitor` |
+| `{{time}}` | 通知产生的时间,如 `09-15 20:16 +08:00`;离线通知比断开晚一个宽限期 |
+
+不认识的 `{{…}}` 原样保留。模板清空后保存,恢复默认。
+
+**有多个 hub** 时,把站点名称设成能区分的名字,模板里加上 `{{site}}`:
+
+```text
+[{{site}}] {{title}}
+{{message}}
+```
+
+**钉钉、飞书要求消息包含关键词**时,把关键词固定写在模板开头,每条消息才都带着它,
+写法见下面「[常见服务的写法](#常见服务的写法)」。
+
+## Webhook
+
+hub 以 `POST` 发送,`Content-Type: application/json`,请求体按模板生成。请求头里写了 `Content-Type`
+的话会替换掉默认值。
+
+**URL 要填最终地址。** hub 不跟随跳转:`http://` 地址被跳到 `https://`、或者少了末尾的 `/`,发送测试会
+直接报 `301 Moved Permanently`,把 URL 改成它跳去的地址即可。跟随跳转的话,POST 会被改成不带内容的
+GET,对方照样回 200,面板显示发送成功而消息根本没到;请求头里的凭证也会被带到跳转后的主机。
+
+在请求体模板里,占位符的值会按 JSON 字符串转义(引号、反斜杠、换行都处理了),所以**必须写在引号
+里面**。写错了预览会直接提示;保存时 hub 也会用一组带引号和换行的样本代入一遍,不是合法 JSON 就
+拒绝保存。
+
+默认请求体:
+
+```json
+{"event":"{{event}}","node":"{{node}}","title":"{{title}}","message":"{{message}}"}
+```
+
+请求头可选,一行一个,写成 `Name: value`,用来带鉴权。
+
+### 常见服务的写法
+
+**Discord**:URL 填频道的 Webhook 地址。
+
+```json
+{"content":"{{title}}\n{{message}}"}
+```
+
+**Slack**:URL 填 Incoming Webhook 地址。
+
+```json
+{"text":"{{title}}\n{{message}}"}
+```
+
+**企业微信群机器人**:URL 填 `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=…`。
+
+```json
+{"msgtype":"text","text":{"content":"{{title}}\n{{message}}"}}
+```
+
+**钉钉群机器人**:URL 填 `https://oapi.dingtalk.com/robot/send?access_token=…`。安全设置选**自定义关键词**,
+只填一个固定的词(比如 `探针`),再把它写在模板开头。别挑只出现在部分消息里的词:
+「通知」只在测试消息里有,「节点」只在多台汇总里有,测试能收到,单台节点的离线、恢复却会被拒收。
+**加签**要按时间戳实时算签名,这里做不到。
+
+```json
+{"msgtype":"text","text":{"content":"【探针】{{title}}\n{{message}}"}}
+```
+
+**飞书群机器人**:同样不能开签名校验,安全设置用**自定义关键词**,做法和钉钉相同。
+
+```json
+{"msg_type":"text","content":{"text":"【探针】{{title}}\n{{message}}"}}
+```
+
+
+ 企业微信、钉钉、飞书拒收消息时(key 或 token 填错、关键词不匹配)仍然返回 HTTP 200,错误码写在响应内容里,
+ hub 分辨不出来,**发送测试**照样显示已发送。配这三家时,以群里真的收到测试消息为准。
+
+
+**Bark**:URL 填 `https://api.day.app/push`。
+
+```json
+{"device_key":"你的 key","title":"{{title}}","body":"{{message}}"}
+```
+
+**ntfy**:URL 填 `https://ntfy.sh`(或自建地址),需要鉴权时请求头加 `Authorization: Bearer …`。
+
+```json
+{"topic":"你的 topic","title":"{{title}}","message":"{{message}}"}
+```
+
+**Gotify**:URL 填 `https://gotify.example.com/message`,请求头加 `X-Gotify-Key: 应用 token`。
+
+```json
+{"title":"{{title}}","message":"{{message}}"}
+```
+
+## 凭证不回读
+
+Bot Token、Webhook URL、Webhook 请求头这三项保存后面板读不回来,只显示「已设置」。输入框留空保存
+表示不改;要停用一个渠道,点那张卡片上的**清除**;只想删掉请求头,点**清除请求头**。
+
+Webhook URL 也算凭证:Discord、Slack、企业微信、钉钉、Bark 的地址本身就是密钥,拿到就能往你的频道
+里发消息。
+
+
+ 数据库备份里包含这些凭证。导出的备份文件按密钥保管。
+
+
+## 收不到通知
+
+先点**发送测试**:各渠道的错误会直接显示在面板上(企业微信、钉钉、飞书除外,看群里有没有收到)。常见的:
+
+| 报错 | 原因 |
+|---|---|
+| `telegram: 401 Unauthorized` | token 填错,或者机器人被删了 |
+| `telegram: 400 Bad Request` … `chat not found` | Chat ID 不对,或者你还没给机器人发过消息、机器人不在群里 |
+| `telegram: 403 Forbidden` … `bot was blocked by the user` | 你屏蔽了机器人 |
+| `webhook: 301 Moved Permanently …` | URL 会跳转,改成它跳去的地址,见上面 Webhook 一节 |
+| `webhook: 4xx …` | 服务端拒收,后面跟着服务端自己的错误信息 |
+| `error sending request` | hub 连不上那个地址 |
+
+测试能收到、真实事件收不到:
+
+- 离线通知要在**离线通知**卡片里打开对应节点,默认是关的。
+- 离线通知来得晚:节点最近 1 小时内有过一次超过宽限期的掉线,这一次要掉满 30 分钟才报,见上面「反复掉线的节点」。
+- 流量提醒要节点填了**每月流量额度**,到期提醒要节点填了**到期日**。
+- 钉钉、飞书只收到一部分:关键词没有出现在每条消息里,见上面「钉钉群机器人」。
+- 看 hub 日志。连不上、对方 5xx 或限流(429)时隔 10 秒再试,最多 3 次;token 错、地址跳转这类重试
+ 也没用的错误不重试。最终没送出去的每条记一行:
+
+```bash
+journalctl -u monitor-hub | grep 'not delivered' # 二进制部署
+docker logs monitor 2>&1 | grep 'not delivered' # 容器部署
+```
diff --git a/src/content/dev/architecture.mdx b/src/content/dev/architecture.mdx
index f2bd5b1..18d94a3 100644
--- a/src/content/dev/architecture.mdx
+++ b/src/content/dev/architecture.mdx
@@ -104,6 +104,7 @@ POST /api/nodes/{id}/token PUT /api/nodes/{id}/traffic
GET /api/ping-tasks POST /api/ping-tasks
DELETE /api/ping-tasks/{id}
GET /api/settings PUT /api/settings
+POST /api/notify/test
GET /api/themes POST /api/themes
DELETE /api/themes/{short} GET /api/themes/{short}/preview
POST /api/themes/{short}/update
@@ -126,7 +127,7 @@ POST /api/db/restore POST /api/db/vacuum
| 表 | 作用 |
|---|---|
| `setting` | key/value 配置,替代配置文件 |
-| `node` | 节点配置 + agent 上报的静态信息 |
+| `node` | 节点配置 + agent 上报的静态信息,含离线通知的开关与状态 |
| `traffic` | **单调递增的流量累计**。1:1 于 node,但每次上报都写 |
| `metric` | 历史明细,每节点每分钟一行,按保留天数删 |
| `ping_task` / `ping_node` | 探测任务及其节点分配 |
diff --git a/src/content/guide/philosophy.mdx b/src/content/guide/philosophy.mdx
index a570419..8e00e51 100644
--- a/src/content/guide/philosophy.mdx
+++ b/src/content/guide/philosophy.mdx
@@ -65,7 +65,7 @@ hub 只有四个命令行参数:`--listen`、`--db`、`--themes`、`--site`。
| 不做 | 原因 |
|---|---|
-| 通知与告警 | 探针的输出是数据,告警是另一个系统的事 |
+| 负载告警 | 阈值、持续时长、节点范围是一整套规则引擎。通知只做掉线、流量、到期、登录这几件判定明确的事 |
| 远程 SSH / web terminal | 只读的观察者被攻破,和能执行命令的工具被攻破,代价差一个数量级 |
| 插件系统 | 等于把第三方代码请进 hub 的进程 |
| ICMP / HTTP 探测 | TCP 一种够用,三种要三套超时语义、三套图例 |
diff --git a/src/nav.ts b/src/nav.ts
index 92c3dde..f021948 100644
--- a/src/nav.ts
+++ b/src/nav.ts
@@ -28,6 +28,7 @@ export const nav: Section[] = [
title: "配置",
items: [
{ path: "/config/auth", label: "登录与安全", desc: "应急密码与 GitHub 单点登录的配置,以及登录不通时的排查路径。", keywords: "github oauth sso 登录 密码 应急 白名单 callback" },
+ { path: "/config/notify", label: "通知", desc: "Telegram 与 Webhook 推送掉线、流量、到期和登录,以及常见服务的请求体写法。", keywords: "通知 告警 telegram tg bot webhook discord slack 钉钉 企业微信 飞书 bark ntfy gotify 离线 掉线 到期" },
{ path: "/config/traffic", label: "流量统计", desc: "三个流量数字的算法、周期与配额口径,以及和商家对不上的原因。", keywords: "流量 traffic 重置日 月流量 计费 sum max 上行 下行 配额" },
],
},