通知增加通信 IP 的展示,移除自定义 Webhook 的默认 json form 模版 fix #42

This commit is contained in:
dushixiang committed 2026-01-25 21:02:09 +08:00
1 parent 8d0e784d64
commit 3f3ddfea49
5 files changed
+66 -242

No files matched your search

+5 -7
View File
@@ -28,8 +28,7 @@ type NotificationChannelConfig struct {
// "url": "https://...",
// "method": "POST", // 可选:GET, POST, PUT, PATCH, DELETE,默认 POST
// "headers": {"key": "value"}, // 可选:自定义请求头
// "bodyTemplate": "json" // 可选:json(默认), form, custom
// "customBody": "" // 当 bodyTemplate 为 custom 时使用,支持变量替换
// "customBody": "" // 自定义请求体模板,支持变量替换
// }
// DNSProviderConfig DNS 服务商配置(存储在 Property 中)
@@ -47,11 +46,10 @@ type DNSProviderConfig struct {
// WebhookConfig 自定义 Webhook 配置结构
type WebhookConfig struct {
URL string `json:"url"` // Webhook URL
Method string `json:"method,omitempty"` // 请求方法,默认 POST
Headers map[string]string `json:"headers,omitempty"` // 自定义请求头
BodyTemplate string `json:"bodyTemplate,omitempty"` // 请求体模板:json, form, custom
CustomBody string `json:"customBody,omitempty"` // 自定义请求体模板(支持变量)
URL string `json:"url"` // Webhook URL
Method string `json:"method,omitempty"` // 请求方法,默认 POST
Headers map[string]string `json:"headers,omitempty"` // 自定义请求头
CustomBody string `json:"customBody,omitempty"` // 自定义请求体模板(支持变量)
}
type SystemConfig struct {
+26 -106
View File
@@ -11,7 +11,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@@ -139,8 +138,11 @@ func maskIPAddress(ip string) string {
return "****"
}
func joinAgentIPs(ipv4 string, ipv6 string) string {
func joinAgentIPs(ip string, ipv4 string, ipv6 string) string {
parts := make([]string, 0, 2)
if ip != "" {
parts = append(parts, ip)
}
if ipv4 != "" {
parts = append(parts, ipv4)
}
@@ -151,9 +153,13 @@ func joinAgentIPs(ipv4 string, ipv6 string) string {
}
func formatAgentIP(agent *models.Agent, mask bool) string {
ip := strings.TrimSpace(agent.IP)
ipv4 := strings.TrimSpace(agent.IPv4)
ipv6 := strings.TrimSpace(agent.IPv6)
if mask {
if ip != "" {
ip = maskIPAddress(ip)
}
if ipv4 != "" {
ipv4 = maskIPAddress(ipv4)
}
@@ -161,7 +167,7 @@ func formatAgentIP(agent *models.Agent, mask bool) string {
ipv6 = maskIPAddress(ipv6)
}
}
combined := joinAgentIPs(ipv4, ipv6)
combined := joinAgentIPs(ip, ipv4, ipv6)
if combined == "" {
return "-"
}
@@ -534,11 +540,10 @@ func (n *Notifier) sendEmail(ctx context.Context, smtpHost string, smtpPort int,
// webhookConfig Webhook 配置
type webhookConfig struct {
URL string
Method string
Headers map[string]string
BodyTemplate string
CustomBody string
URL string
Method string
Headers map[string]string
CustomBody string
}
// parseWebhookConfig 解析 Webhook 配置
@@ -565,86 +570,21 @@ func parseWebhookConfig(config map[string]interface{}) (*webhookConfig, error) {
}
}
// 获取请求体模板类型,默认 json
bodyTemplate := "json"
if bt, ok := config["bodyTemplate"].(string); ok && bt != "" {
bodyTemplate = bt
}
// 获取自定义请求体
customBody, _ := config["customBody"].(string)
return &webhookConfig{
URL: webhookURL,
Method: method,
Headers: headers,
BodyTemplate: bodyTemplate,
CustomBody: customBody,
URL: webhookURL,
Method: method,
Headers: headers,
CustomBody: customBody,
}, nil
}
// buildJSONBody 构建 JSON 格式的请求体
func (n *Notifier) buildJSONBody(agent *models.Agent, record *models.AlertRecord, message string) (io.Reader, error) {
agentIP := joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
body := map[string]interface{}{
"msg_type": "text",
"text": map[string]string{
"content": message,
},
"agent": map[string]interface{}{
"id": agent.ID,
"name": agent.Name,
"hostname": agent.Hostname,
"ip": agentIP,
"ipv4": agent.IPv4,
"ipv6": agent.IPv6,
},
"alert": map[string]interface{}{
"type": record.AlertType,
"level": record.Level,
"status": record.Status,
"message": record.Message,
"threshold": record.Threshold,
"actualValue": record.ActualValue,
"firedAt": record.FiredAt,
"resolvedAt": record.ResolvedAt,
},
}
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("序列化 JSON 失败: %w", err)
}
return bytes.NewReader(data), nil
}
// buildFormBody 构建 Form 表单格式的请求体
func (n *Notifier) buildFormBody(agent *models.Agent, record *models.AlertRecord, message string) io.Reader {
agentIP := joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
formData := url.Values{}
formData.Set("message", message)
formData.Set("agent_id", agent.ID)
formData.Set("agent_name", agent.Name)
formData.Set("agent_hostname", agent.Hostname)
formData.Set("agent_ip", agentIP)
formData.Set("agent_ipv4", agent.IPv4)
formData.Set("agent_ipv6", agent.IPv6)
formData.Set("alert_type", record.AlertType)
formData.Set("alert_level", record.Level)
formData.Set("alert_status", record.Status)
formData.Set("alert_message", record.Message)
formData.Set("threshold", fmt.Sprintf("%.2f", record.Threshold))
formData.Set("actual_value", fmt.Sprintf("%.2f", record.ActualValue))
formData.Set("fired_at", fmt.Sprintf("%d", record.FiredAt))
if record.ResolvedAt > 0 {
formData.Set("resolved_at", fmt.Sprintf("%d", record.ResolvedAt))
}
return strings.NewReader(formData.Encode())
}
// buildCustomBody 构建自定义模板格式的请求体
func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertRecord, message, customBody string) (io.Reader, error) {
func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertRecord, message, customBody string, maskIP bool) (io.Reader, error) {
if customBody == "" {
return nil, fmt.Errorf("使用 custom 模板时必须提供 customBody")
return nil, fmt.Errorf("必须提供自定义请求体模板")
}
// 使用 fasttemplate 进行变量替换
@@ -669,7 +609,7 @@ func (n *Notifier) buildCustomBody(agent *models.Agent, record *models.AlertReco
case "agent.hostname":
v = agent.Hostname
case "agent.ip":
v = joinAgentIPs(strings.TrimSpace(agent.IPv4), strings.TrimSpace(agent.IPv6))
v = agent.IP
case "agent.ipv4":
v = agent.IPv4
case "agent.ipv6":
@@ -758,34 +698,14 @@ func (n *Notifier) sendCustomWebhook(ctx context.Context, config map[string]inte
// 构建消息内容
message := n.buildMessage(agent, record, maskIP)
// 根据模板类型构建请求体
var reqBody io.Reader
var contentType string
switch cfg.BodyTemplate {
case "json":
reqBody, err = n.buildJSONBody(agent, record, message)
if err != nil {
return err
}
contentType = "application/json"
case "form":
reqBody = n.buildFormBody(agent, record, message)
contentType = "application/x-www-form-urlencoded"
case "custom":
reqBody, err = n.buildCustomBody(agent, record, message, cfg.CustomBody)
if err != nil {
return err
}
contentType = "text/plain"
default:
return fmt.Errorf("不支持的 bodyTemplate: %s", cfg.BodyTemplate)
// 构建自定义请求体
reqBody, err := n.buildCustomBody(agent, record, message, cfg.CustomBody, maskIP)
if err != nil {
return err
}
// 发送 HTTP 请求
// 发送 HTTP 请求,使用 application/json 作为默认 Content-Type
contentType := "application/json"
return n.sendHTTPRequest(ctx, cfg.Method, cfg.URL, reqBody, cfg.Headers, contentType)
}
-2
View File
@@ -22,8 +22,6 @@ const (
PropertyIDNotificationChannels = "notification_channels"
// PropertyIDSystemConfig 系统配置的固定 ID
PropertyIDSystemConfig = "system_config"
// PropertyIDMetricsConfig 指标配置的固定 ID
PropertyIDMetricsConfig = "metrics_config"
// PropertyIDPublicIPConfig 公网 IP 采集配置的固定 ID
PropertyIDPublicIPConfig = "public_ip_config"
// PropertyIDAlertConfig 告警配置的固定 ID
@@ -103,7 +103,6 @@ const NotificationChannels = () => {
formValues.webhookEnabled = channel.enabled;
formValues.webhookUrl = channel.config?.url || '';
formValues.webhookMethod = channel.config?.method || 'POST';
formValues.webhookBodyTemplate = channel.config?.bodyTemplate || 'json';
formValues.webhookCustomBody = channel.config?.customBody || '';
// 解析 headers 为数组形式方便编辑
@@ -223,7 +222,6 @@ const NotificationChannels = () => {
config: {
url: values.webhookUrl || '',
method: values.webhookMethod || 'POST',
bodyTemplate: values.webhookBodyTemplate || 'json',
customBody: values.webhookCustomBody || '',
headers: Object.keys(headersObj).length > 0 ? headersObj : undefined,
},
@@ -715,8 +713,7 @@ const NotificationChannels = () => {
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.webhookEnabled !== currentValues.webhookEnabled ||
prevValues.webhookBodyTemplate !== currentValues.webhookBodyTemplate
prevValues.webhookEnabled !== currentValues.webhookEnabled
}
>
{({getFieldValue}) =>
@@ -750,51 +747,24 @@ const NotificationChannels = () => {
/>
</Form.Item>
{/* 请求体模板 */}
{/* 自定义请求体 */}
<Form.Item
label="请求体模板"
name="webhookBodyTemplate"
tooltip="选择请求体的格式"
label="自定义请求体"
name="webhookCustomBody"
rules={[
{
required: true,
message: '请输入自定义请求体模板'
}
]}
tooltip="支持变量替换,可用变量见下方说明"
>
<Select
placeholder="选择请求体模板"
options={[
{
label: 'JSON (默认)',
value: 'json'
},
{
label: 'Form 表单',
value: 'form'
},
{
label: '自定义模板',
value: 'custom'
},
]}
<Input.TextArea
rows={6}
placeholder='示例: {"alert": "{{alert.message}}", "host": "{{agent.hostname}}"}'
/>
</Form.Item>
{/* 自定义请求体 */}
{getFieldValue('webhookBodyTemplate') === 'custom' && (
<Form.Item
label="自定义请求体"
name="webhookCustomBody"
rules={[
{
required: true,
message: '请输入自定义请求体模板'
}
]}
tooltip="支持变量替换,可用变量见下方说明"
>
<Input.TextArea
rows={6}
placeholder='示例: {"alert": "{{alert.message}}", "host": "{{agent.hostname}}"}'
/>
</Form.Item>
)}
{/* 自定义请求头 */}
<Form.Item label="自定义请求头"
tooltip="添加自定义 HTTP 请求头">
@@ -6,97 +6,35 @@ const NotificationCustomHelp = () => {
const items: CollapseProps['items'] = [
{
key: 'help',
label: '请求体格式说明',
label: '自定义请求体模板说明',
children: <div className={'space-y-3 text-sm'}>
{/* JSON 格式说明 */}
<div className={'space-y-1'}>
<strong>1. JSON 格式 (默认):</strong>
<div className={'text-gray-600 text-xs'}>
发送 <code className={'bg-gray-100 px-1 rounded'}>application/json</code> 格式的数据
</div>
<pre className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
{JSON.stringify({
"msg_type": "text",
"text": {"content": "告警消息内容"},
"agent": {
"id": "agent-id",
"name": "探针名称",
"hostname": "主机名",
"ip": "192.168.1.1"
},
"alert": {
"type": "cpu",
"level": "warning",
"status": "firing",
"message": "CPU使用率过高",
"threshold": 80,
"actualValue": 85.5,
"firedAt": 1234567890000,
"resolvedAt": 0
}
}, null, 2)}
</pre>
</div>
{/* Form 表单格式说明 */}
<div className={'space-y-1'}>
<strong>2. Form 表单格式:</strong>
<div className={'text-gray-600 text-xs'}>
发送 <code
className={'bg-gray-100 px-1 rounded'}>application/x-www-form-urlencoded</code> 格式的数据
</div>
<div className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
<div className={'font-semibold mb-1'}>包含以下字段:</div>
<div className={'grid grid-cols-2 gap-x-4 gap-y-1'}>
<div>• <code>message</code> - 告警消息</div>
<div>• <code>agent_id</code> - 探针ID</div>
<div>• <code>agent_name</code> - 探针名称</div>
<div>• <code>agent_hostname</code> - 主机名</div>
<div>• <code>agent_ip</code> - IP地址(合并)</div>
<div>• <code>agent_ipv4</code> - IPv4 地址</div>
<div>• <code>agent_ipv6</code> - IPv6 地址</div>
<div>• <code>alert_type</code> - 告警类型</div>
<div>• <code>alert_level</code> - 告警级别</div>
<div>• <code>alert_status</code> - 告警状态</div>
<div>• <code>alert_message</code> - 告警详情</div>
<div>• <code>threshold</code> - 阈值</div>
<div>• <code>actual_value</code> - 当前值</div>
<div>• <code>fired_at</code> - 触发时间(格式化)</div>
<div>• <code>resolved_at</code> - 恢复时间(格式化)</div>
</div>
</div>
</div>
{/* 自定义模板说明 */}
<div className={'space-y-1'}>
<strong>3. 自定义模板:</strong>
<div className={'text-gray-600 text-xs'}>
支持变量替换,Content-Type 为 <code
className={'bg-gray-100 px-1 rounded'}>text/plain</code>
<div className={'text-gray-600 dark:text-gray-400 text-xs'}>
支持变量替换,Content-Type 为 <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>application/json</code>
</div>
<div className={'border p-2 rounded-md text-xs mt-1 bg-gray-50'}>
<div className={'border dark:border-gray-700 p-2 rounded-md text-xs mt-1 bg-gray-50 dark:bg-gray-800'}>
<div className={'font-semibold mb-1'}>可用变量:</div>
<div className={'grid grid-cols-2 gap-x-4 gap-y-1'}>
<div>• <code>{`{{message}}`}</code> - 告警消息</div>
<div>• <code>{`{{agent.id}}`}</code> - 探针ID</div>
<div>• <code>{`{{agent.name}}`}</code> - 探针名称</div>
<div>• <code>{`{{agent.hostname}}`}</code> - 主机名</div>
<div>• <code>{`{{agent.ip}}`}</code> - IP地址(合并)</div>
<div>• <code>{`{{agent.ipv4}}`}</code> - IPv4 地址</div>
<div>• <code>{`{{agent.ipv6}}`}</code> - IPv6 地址</div>
<div>• <code>{`{{alert.type}}`}</code> - 告警类型</div>
<div>• <code>{`{{alert.level}}`}</code> - 告警级别</div>
<div>• <code>{`{{alert.status}}`}</code> - 告警状态</div>
<div>• <code>{`{{alert.message}}`}</code> - 告警消息</div>
<div>• <code>{`{{alert.threshold}}`}</code> - 阈值</div>
<div>• <code>{`{{alert.actualValue}}`}</code> - 当前值</div>
<div>• <code>{`{{alert.firedAt}}`}</code> - 触发时间(格式化)</div>
<div>• <code>{`{{alert.resolvedAt}}`}</code> - 恢复时间(格式化)</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{message}}`}</code> - 告警消息</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.id}}`}</code> - 探针ID</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.name}}`}</code> - 探针名称</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.hostname}}`}</code> - 主机名</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ip}}`}</code> - IP地址(合并)</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ipv4}}`}</code> - IPv4 地址</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{agent.ipv6}}`}</code> - IPv6 地址</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.type}}`}</code> - 告警类型</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.level}}`}</code> - 告警级别</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.status}}`}</code> - 告警状态</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.message}}`}</code> - 告警消息</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.threshold}}`}</code> - 阈值</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.actualValue}}`}</code> - 当前值</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.firedAt}}`}</code> - 触发时间(格式化)</div>
<div>• <code className={'bg-gray-100 dark:bg-gray-700 px-1 rounded'}>{`{{alert.resolvedAt}}`}</code> - 恢复时间(格式化)</div>
</div>
<div className={'mt-2 pt-2 border-t'}>
<div className={'mt-2 pt-2 border-t dark:border-gray-700'}>
<div className={'font-semibold mb-1'}>示例:</div>
<pre className={'text-xs'}>
<pre className={'text-xs bg-gray-100 dark:bg-gray-700 p-2 rounded'}>
{`{
"alert": "{{alert.message}}",
"host": "{{agent.hostname}}",