with one click
n9e-generate-message-template
// 生成或修改夜莺(n9e)告警通知消息模板。当用户要求写通知模板、改消息格式、加主机名/恢复值/级别、钉钉/飞书/Lark/邮件/短信/电话模板时使用。
// 生成或修改夜莺(n9e)告警通知消息模板。当用户要求写通知模板、改消息格式、加主机名/恢复值/级别、钉钉/飞书/Lark/邮件/短信/电话模板时使用。
This skill should be used when the user asks "how-to" or factual questions about the 夜莺(n9e) / Flashcat platform — UI/where-to-click, 业务组/订阅规则/屏蔽规则/edge 模式, Token 使用, 通知 pipeline, 自愈触发条件; OR about categraf input plugin field meanings, metric names, defaults, environment variables, config syntax (e.g. "[[instances]] 怎么写", "ping_average_response_ms 单位"). NOT for actively troubleshooting an alert or querying metrics.
This skill should be used when the user asks to "troubleshoot", "diagnose", "debug alert", "investigate incident", "故障定位", "告警排查", "问题诊断", "排障", "查告警", "分析告警", "根因分析", "查指标", "查日志", or discusses monitoring/alerting/observability issues in 夜莺(n9e) platform.
**创建单条告警规则**(用户描述一个告警需求 → 建一条规则)。支持 Prometheus / Loki / ES / OpenSearch / MySQL / PG / TDengine / ClickHouse / Doris / VictoriaLogs / Host 全部数据源。 ⚠️ **不要用这个 skill 做批量导入**——用户给的是 URL 或 YAML 文件、awesome-prometheus-alerts、node-exporter.yml 之类,请改用 n9e-import-prom-rule。 触发:创建一条 / 加一条告警 / 帮我建个 CPU 告警 / 给 MySQL 加个告警规则 / 我要监控某个指标。
**批量导入 Prometheus 告警规则 YAML 文件**到夜莺(一次性建一组规则)。专用于处理远端 URL 或本地 YAML 文本,自动解析 `groups` / 纯 `rules` 数组 / 单条 rule 三种格式。 ⚠️ **不要用这个 skill 做单条创建**——用户用自然语言描述一条告警需求时,请改用 n9e-create-alert-rule。 触发:导入 / import / 批量 / URL / .yml 文件 / .yaml 文件 / awesome-prometheus-alerts / node-exporter.yml / prometheus rule file。
This skill should be used when the user reports that an alert rule is "not firing", "没发告警", "告警不触发", "规则没生效", "应该报警但没报警", "为什么没收到告警", "alert rule not firing", or wants to diagnose why a specific alert rule failed to produce an event/notification. 适用于排查"告警规则为什么没正常发出告警",而不是看已有告警找根因(后者用 ops-troubleshooting)。仅支持 Release 22 及以上版本。
解答"如何部署 categraf 采集器"。触发场景:用户问"怎么装 categraf / categraf 怎么部署 / 用 Docker 跑 categraf / Windows 装 categraf / categraf 怎么注册成系统服务 / categraf 上报到夜莺 / categraf config.toml 怎么写 / 怎么验证 categraf 采集到数据"。覆盖二进制+systemd、Docker、Windows、K8s 提示、关键配置、常见验证命令。本 skill 是教学/指引型,不调任何工具,输出可粘贴执行的命令与配置片段。
| name | n9e-generate-message-template |
| description | 生成或修改夜莺(n9e)告警通知消息模板。当用户要求写通知模板、改消息格式、加主机名/恢复值/级别、钉钉/飞书/Lark/邮件/短信/电话模板时使用。 |
夜莺的消息模板是 Go text/template / html/template 语法(邮件走 text/template,其他走 html/template 再做转义)。用户在「通知管理 → 消息模板」页面编辑,保存后被通知规则引用,触发告警时按渲染数据替换变量后发送到各通道。
本技能专注于写/改模板片段本身,不涉及创建通知规则或通道配置。
后端执行时传入的 renderData:
| key | 类型 | 说明 |
|---|---|---|
.events | []*AlertCurEvent | 当前批次的告警事件列表,通常只有 1 条 |
.domain | string | n9e 站点 URL,用于拼跳转链接 |
自动注入的简写(不用手写):
{{ $events := .events }}
{{ $event := index $events 0 }}
{{ $labels := $event.TagsMap }}
{{ $value := $event.TriggerValue }}
所以模板里可以直接使用 $event.xxx、$labels.agent_hostname、$value。
$event 可用字段(*AlertCurEvent)| 字段 | 类型 | 说明 |
|---|---|---|
.Id | int64 | 告警事件 ID(拼跳转链用) |
.RuleId | int64 | 告警规则 ID |
.RuleName | string | 规则名称 |
.RuleNote | string | 规则备注 |
.Severity | int | 告警级别:1=Critical, 2=Warning, 3=Info |
.PromQl | string | 告警触发表达式 |
.RuleAlgo | string | 规则算法类型 |
.TriggerTime | int64 | 触发时间 unix 秒 |
.TriggerValue | string | 触发时的指标值(已为字符串) |
.FirstTriggerTime | int64 | 首次异常时间(连续告警) |
.LastEvalTime | int64 | 最近一次评估时间,恢复时作为恢复时间使用 |
.IsRecovered | bool | 是否已恢复 |
.NotifyCurNumber | int | 本次通知是第几次发送 |
.TargetIdent | string | 监控对象(通常是 agent_hostname) |
.TargetNote | string | 对象备注 |
.GroupId / .GroupName | int64 / string | 业务组 |
.Cluster | string | 数据源集群名 |
.Cate | string | 数据源类型(prometheus / host / mysql / ...) |
.RunbookUrl | string | 运行手册链接 |
| 字段 | 类型 | 说明 |
|---|---|---|
.TagsMap | map[string]string | 事件标签,优先用:{{$labels.agent_hostname}} |
.TagsJSON | []string | 形如 ["k=v", ...] 的字符串数组(早期模板常用) |
.AnnotationsJSON | map[string]string | 注解,常用 .AnnotationsJSON.recovery_value |
.TriggerValuesJson.ValuesWithUnit | map | 带单位的触发值(多值场景) |
| 字段 | 类型 | 说明 |
|---|---|---|
.NotifyUsersObj | []*User | 本次要通知的用户对象(有 Username/Nickname/Phone/Email) |
.NotifyGroupsObj | []*UserGroup | 关联用户组 |
用户对象在钉钉/飞书/Lark 模板里常用来拼 at,见下文
ats系列 helper。
tplx.TemplateFuncMap)timeformat <unix> — 默认 "2006-01-02 15:04:05";传第二个参数覆盖:{{timeformat $event.TriggerTime "15:04:05"}}timestamp — 当前时间字符串now.Unix — 当前 unix 秒(now 是 Go template 内置)humanizeDuration <sec> — "3m15s"humanizeDurationInterface <interface> — 同上但接受 interfacetoTime <unix> — 返回 time.Time,可链式调用parseDuration <"5m"> — 返回 time.DurationformatDecimal <v> <n> — 保留 n 位小数({{formatDecimal $event.TriggerValue 2}})humanize <v> — K/M/G 单位(1000 进制,SI)humanize1024 <v> — Ki/Mi/Gi(1024 进制)humanizePercentage <v> / humanizePercentageH <v> — 百分比add / sub / mul / div <a> <b> — 四则printf "%.2f" <v> — Go fmt.SprintftoUpper / toLower / title — 大小写contains <s> <sub> — 子串match <regex> <s> — 正则匹配(bool)reReplaceAll <regex> <repl> <s> — 正则替换split <s> <sep> / join <slice> <sep>stripPort <host:port> / stripDomain <host.domain>b64enc / b64dec — base64escape <s> — URL 路径转义unescaped <s> — 输出 raw HTML(不转义)safeHtml <s> — 同上urlconvert <s>label <key> <labelMap> / value <key> <m> / strvalue <v>first <slice> — 取第一个tagsMapToStr <map> — 标签拼成 k=v,k=vsortByLabel <items> <key> — 按标签排序ats <users> <platform> — 生成 at 片段batchContactsAts <contacts> <platform> — 批量 atbatchContactsAtsInFeishuEmail <contacts> / batchContactsAtsInFeishuId <contacts> — 飞书专用batchContactsJoinComma <contacts> / batchContactsJsonMarshal <contacts>mappingAndJoin <map> <kvSep> <itemSep>jsonMarshal <v> — 序列化为 JSON 字符串mapDifference <a> <b> — 集合差notify_channel_ident)语法差异消息模板绑在具体通道上,通道 ident 决定文本引擎和转义行为:
| Ident | 引擎 | 说明 |
|---|---|---|
email | text/template | 不再次转义,HTML 模板直接写标签 |
slackwebhook / slackbot | html/template | 渲染后把 " / \n 转义并包成 template.HTML;通常写 Markdown |
其它(dingtalk / feishu / feishucard / larkcard / wecom / tx-sms / ali-voice …) | html/template | 渲染后对 " \n \r 做 JSON 字符串转义,适合直接塞入 webhook payload |
重要:
html/template会对<>&自动转义。不想转义的地方要用{{unescaped "…"}}或{{safeHtml .X}},否则钉钉里渲染出来会看到字面<。
dingtalk,"飞书卡片"是 feishucard,"邮件"是 email。不同通道风格差异大。$event.IsRecovered 两个分支。只有短信/语音可以省略。$labels.<key>,而不是 $event.TagsJSON。$event.TriggerValue;要两位小数就 {{formatDecimal $event.TriggerValue 2}}。timeformat。{{.domain}}/share/alert-his-events/{{$event.Id}}。{{$event.Severity}}{{if eq $event.Severity 1}}一级{{else if eq $event.Severity 2}}二级{{else}}三级{{end}}Critical / Warning / Info@全员:在模板末尾加一行 @all(钉钉按空格分词匹配)。精确 at 手机号:{{range $event.NotifyUsersObj}}@{{.Phone}} {{end}}。{{batchContactsAtsInFeishuEmail $event.NotifyUsersObj}} 拼出 <at email=...></at> 片段。 gotemplate ``` 包裹模板主体,末尾给一段变量/函数说明,每个非平凡变量/函数单独一行。回复给用户时:
<模板内容>
**变量说明**:列出模板里用到的非平凡字段和函数($event.TargetIdent、formatDecimal、timeformat 之类),每项一行。target_ident 还是某个 tag),在导语里直接点出做了什么假设,不要反问。语言跟随用户输入(中文输入就用中文)。
#### {{if $event.IsRecovered}}<font color="#008800">💚{{$event.RuleName}}</font>{{else}}<font color="#FF0000">💔{{$event.RuleName}}</font>{{end}}
---
{{$duration := sub now.Unix $event.FirstTriggerTime}}{{if $event.IsRecovered}}{{$duration = sub $event.LastEvalTime $event.FirstTriggerTime}}{{end}}
- **告警级别**: S{{$event.Severity}}
{{- if $event.RuleNote}}
- **规则备注**: {{$event.RuleNote}}
{{- end}}
{{- if $event.TargetIdent}}
- **监控对象**: {{$event.TargetIdent}}
{{- end}}
{{- if not $event.IsRecovered}}
- **触发时值**: {{$event.TriggerValue}}
- **触发时间**: {{timeformat $event.TriggerTime}}
- **持续时长**: {{humanizeDurationInterface $duration}}
{{- else}}
- **恢复时间**: {{timeformat $event.LastEvalTime}}
- **持续时长**: {{humanizeDurationInterface $duration}}
{{- end}}
- **事件标签**:
{{- range $k, $v := $labels}}
{{- if ne $k "rulename"}}
- {{$k}}: {{$v}}
{{- end}}
{{- end}}
[事件详情]({{.domain}}/share/alert-his-events/{{$event.Id}}) | [屏蔽1小时]({{.domain}}/alert-mutes/add?__event_id={{$event.Id}})
{{- if $event.IsRecovered}}
**级别状态:** S{{$event.Severity}} Recovered
**告警名称:** {{$event.RuleName}}
**事件标签:** {{$event.TagsJSON}}
**恢复时间:** {{timeformat $event.LastEvalTime}}
{{- else}}
**级别状态:** S{{$event.Severity}} Triggered
**告警名称:** {{$event.RuleName}}
**事件标签:** {{$event.TagsJSON}}
**触发时间:** {{timeformat $event.TriggerTime}}
**触发时值:** {{$event.TriggerValue}}
{{- if $event.RuleNote}}
**告警描述:** {{$event.RuleNote}}
{{- end}}
{{- end}}
级别状态: S{{$event.Severity}} {{if $event.IsRecovered}}Recovered{{else}}Triggered{{end}} 规则: {{$event.RuleName}} 对象: {{$event.TargetIdent}}
- **主机**: {{$event.TargetIdent}}
{{- if $labels.ip}}
- **IP**: {{$labels.ip}}
{{- end}}
说明:
target_ident一般就是主机名;如果需要 IP,优先看标签ip、instance、host。
- **触发时值**: {{formatDecimal $event.TriggerValue 2}}
TriggerValue本身是字符串,formatDecimal会先转浮点再格式化,非数字会原样返回。
...模板正文...
{{- range $event.NotifyUsersObj}}@{{.Phone}} {{end}}
钉钉按 "空格 + 手机号" 识别被 at 的用户。或者统一
@all。
{{- if $event.IsRecovered}}
{{- if $event.AnnotationsJSON.recovery_value}}
- **恢复时值**: {{formatDecimal $event.AnnotationsJSON.recovery_value 4}}
{{- end}}
- **恢复时间**: {{timeformat $event.LastEvalTime}}
{{- end}}
恢复值由告警引擎在恢复时写入
AnnotationsJSON.recovery_value,仅在有恢复值的情况下存在,用if保护。
- **级别**: {{if eq $event.Severity 1}}一级(紧急){{else if eq $event.Severity 2}}二级(重要){{else}}三级(提示){{end}}
模板本身不做过滤——过滤应放在通知规则的 attributes / label_keys。模板里只负责展示。如果用户问到这里要提示一下。
钉钉/邮件用 <font color>,飞书卡片靠 template 字段填颜色 keyword:
{{/* 钉钉 markdown:标题前置 emoji + 颜色 */}}
#### {{if $event.IsRecovered}}<font color="#008800">✅ {{$event.RuleName}}</font>{{else}}<font color="#FF0000">🚨 {{$event.RuleName}}</font>{{end}}
{{/* 飞书 feishucard 的 template 字段(决定卡片头部色块) */}}
{{if $event.IsRecovered}}turquoise{{else}}{{if eq $event.Severity 1}}red{{else if eq $event.Severity 2}}orange{{else}}grey{{end}}{{end}}
飞书卡片只认枚举色:
red / orange / yellow / green / turquoise / blue / indigo / purple / carmine / grey,hex 颜色码会被忽略。
{{$duration := sub now.Unix $event.FirstTriggerTime}}
{{- if $event.IsRecovered}}{{$duration = sub $event.LastEvalTime $event.FirstTriggerTime}}{{end}}
- **持续**: {{humanizeDurationInterface $duration}}
触发态:
当前时间 - FirstTriggerTime;恢复态:LastEvalTime - FirstTriggerTime。humanizeDurationInterface输出1h3m5s这种人类可读形式。
[查看仪表盘]({{.domain}}/dashboards/123?ident={{urlquery $event.TargetIdent}}&host={{urlquery (index $labels "host")}})
[屏蔽1小时]({{.domain}}/alert-mutes/add?__event_id={{$event.Id}})
直接
{{$event.TargetIdent}}拼进 URL,遇到空格、中文或&会被 IM 端二次转义(典型表现:&变&,链接打不开——见 issue #1530、#2396)。所有进入 URL query 的变量都用urlquery包。
- **应用**: {{index $labels "app-name"}}
- **K8s 域**: {{index $labels "k8s.io/cluster"}}
- **业务面板**: {{index $event.AnnotationsJSON "dashboard_url"}}
$labels.app-name会被 Go template 解析成"app减name"必然失败。Key 只要不是纯字母数字下划线,一律index取,Annotations 同理。
- **触发时值**: {{if or (eq $event.TriggerValue "+Inf") (eq $event.TriggerValue "NaN")}}N/A{{else}}{{formatDecimal $event.TriggerValue 2}}{{end}}
PromQL 的
/0会返回+Inf,缺数据的聚合会返回NaN。直接渲染到飞书/钉钉里就是字面+Inf,看着像 bug(issue #2 群里多次反馈)。
{{if gt $event.Id 0}}
[事件详情]({{.domain}}/share/alert-his-events/{{$event.Id}})
{{else}}
(Edge 模式事件,请到中心端查看详情)
{{end}}
已知问题 #1707:Edge 模式下事件 Id 写入是异步的,渲染时拿到 0,跳转链接会落到
/alert-his-events/0(404)。模板层加gt $event.Id 0守护。
{{range $i, $e := .events}}
{{- if $i}}
---
{{end -}}
- 规则:{{$e.RuleName}}
- 对象:{{$e.TargetIdent}}
- 触发值:{{$e.TriggerValue}}
- 时间:{{timeformat $e.TriggerTime}}
{{end}}
默认场景下一条通知就一个事件,模板按
$event = index .events 0渲染。当一条通知聚合了多个事件(订阅聚合、批量发送)时必须range .events才能展开。
html/template 会 HTML 转义。钉钉/飞书/Lark 里 <font>、<at> 等标签必须用 unescaped 包裹或放在内容里由 Go template 自己识别。邮件走 text/template 不受此限。TriggerValue 是字符串:直接比较大小要用 parseDuration/自定义,常规只做显示就好。{{$events := .events}} 等头部——系统会自动注入。NotifyRecovered=1 的规则会复用同一模板发恢复消息。$labels.agent_hostname 能用,但 $labels.app-name 不行,要用 index $labels "app-name"。int64),不要直接 {{$event.TriggerTime}} 当文本,用 timeformat 或 toTime。.events 可能有多条,钉钉/飞书默认模板只渲染第 0 条($event);如果用户要批量展示,用 {{range .events}}。.domain:不要硬编码 http://localhost:17000,否则切环境就坏。❌ {{$event.TriggerValue | printf "%.2f"}} — printf 第一个参数才是 format,且 TriggerValue 是 string。
✅ {{formatDecimal $event.TriggerValue 2}}
❌ {{$event.TriggerTime}} 直接展示 — 会输出 unix 秒。
✅ {{timeformat $event.TriggerTime}}
❌ 钉钉里写 <font color="red"> 不转义 — html/template 会转义成 <font>。
✅ 放在条件分支里(Go template 的 {{if}} 分支字面量不会被 HTML 转义内部标签),或整体 {{unescaped "…"}}。夜莺官方钉钉模板直接写 <font> 能生效,是因为外层是纯文本区段(不在属性值里)——遵循官方样例即可。
❌ 忘记分恢复分支 — 恢复通知里触发时间误导人。
✅ 所有动态内容先用 {{if $event.IsRecovered}}…{{else}}…{{end}} 包好。
❌ {{if .IsRecoverd}} — 拼写少一个 e。Go template 对不存在的字段静默走 else 分支(不报错),后果是恢复通知发的是触发态文案,且很难定位。
✅ {{if $event.IsRecovered}}。同类拼写陷阱:Resoverd Recoverd recovered(小写)都不行。
❌ {{if lt $value 10}} 或 {{if gt $event.TriggerValue 80}} — TriggerValue 是 string,lt/gt 跟数字比较会报 incompatible types,或被解释成字符串字典序得到错误结果。当前版本没有内置 toFloat helper(issue #3090 待合并)。数值阈值判断请放回告警规则的条件表达式里,模板层只做展示。
❌ 拼 URL 时直接 ?ident={{$event.TargetIdent}} — TargetIdent 含空格/中文/& 会破坏链接。
✅ ?ident={{urlquery $event.TargetIdent}},所有进入 URL query 的变量都过 urlquery。
❌ {{$labels.app-name}} 或 {{$labels.k8s.io/cluster}} — 含中划线/点/斜杠的 key 会被解析失败。
✅ {{index $labels "app-name"}}、{{index $labels "k8s.io/cluster"}}。