feat: v0.34 健康探针(Architecture B) + Agent v2.1 + 端口监控 + 告警联系人重构

- v0.31: AlertContact 重构(channel_type下拉+webhook_url替代三分立字段)
- v0.32: Agent ProcessTop 增强(cmdline/user/cpu_pct/mem_pct)
- v0.33: 端口监控 Tab(监听端口自动发现+down联动告警)
- v0.34: 健康探针(Agent本地执行HTTP/TCP/Exec→结果随心跳上报)
- Agent新增: ContextSwitch/FD/HealthProbe采集器
- 后端新增: health_probe handler + Agent model扩展
- HostMonitor新增Tab: 健康探针/端口监控
This commit is contained in:
attacker committed 2026-06-24 19:18:10 +08:00
1 parent e1f75f0bc4
commit d656520428
25 files changed
+1700 -619

No files matched your search

@@ -1,4 +1,4 @@
// 报警联系人创建/编辑对话框 // Logan.Li
// 报警联系人创建/编辑对话框 — 渠道类型下拉 + webhook URL // Logan.Li
import React, { useState, useEffect } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
@@ -6,10 +6,20 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { toast } from 'sonner';
import { alertApi } from '@/services/api';
import type { Contact } from './alert-center-helpers';
const CHANNEL_OPTIONS = [
{ value: 'feishu', label: '飞书', placeholder: 'https://open.feishu.cn/open-apis/bot/v2/hook/...' },
{ value: 'dingtalk', label: '钉钉', placeholder: 'https://oapi.dingtalk.com/robot/send?access_token=...' },
{ value: 'wechat', label: '企业微信', placeholder: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=...' },
{ value: 'webhook', label: '自定义Webhook', placeholder: 'https://your-webhook-url...' },
];
export function ContactDialog({
open,
onOpenChange,
@@ -21,39 +31,37 @@ export function ContactDialog({
editingContact: Contact | null;
onSaved: () => void;
}) {
const [form, setForm] = useState({
name: '', phone: '', email: '', dingtalk: '', feishu: '', wechat: '',
});
const [channelType, setChannelType] = useState('feishu');
const [form, setForm] = useState({ name: '', phone: '', email: '', webhook_url: '' });
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) {
if (editingContact) {
setChannelType(editingContact.channel_type || 'feishu');
setForm({
name: editingContact.name || '',
phone: editingContact.phone || '',
email: editingContact.email || '',
dingtalk: editingContact.dingtalk || '',
feishu: editingContact.feishu || '',
wechat: editingContact.wechat || '',
webhook_url: editingContact.webhook_url || '',
});
} else {
setForm({ name: '', phone: '', email: '', dingtalk: '', feishu: '', wechat: '' });
setChannelType('feishu');
setForm({ name: '', phone: '', email: '', webhook_url: '' });
}
}
}, [open, editingContact]);
const handleSave = async () => {
if (!form.name.trim()) return;
if (!form.name.trim() || !form.webhook_url.trim()) return;
setSaving(true);
try {
const payload = {
name: form.name.trim(),
phone: form.phone.trim() || undefined,
email: form.email.trim() || undefined,
dingtalk: form.dingtalk.trim() || undefined,
feishu: form.feishu.trim() || undefined,
wechat: form.wechat.trim() || undefined,
channel_type: channelType,
webhook_url: form.webhook_url.trim(),
};
if (editingContact) {
await alertApi.updateContact(editingContact.id, payload);
@@ -71,12 +79,14 @@ export function ContactDialog({
}
};
const selectedChannel = CHANNEL_OPTIONS.find(c => c.value === channelType);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingContact ? '编辑报警联系人' : '添加报警联系人'}</DialogTitle>
<DialogDescription>配置报警联系人信息(姓名必填,渠道选填)</DialogDescription>
<DialogDescription>选择通知渠道并填入 Webhook 地址</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
@@ -87,53 +97,50 @@ export function ContactDialog({
placeholder="如:张三"
/>
</div>
<div className="space-y-2">
<Label>手机号</Label>
<Input
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="如:13800138000"
/>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2">
<Label>手机号</Label>
<Input
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="13800138000"
/>
</div>
<div className="space-y-2">
<Label>邮箱</Label>
<Input
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
placeholder="zhangsan@example.com"
/>
</div>
</div>
<div className="space-y-2">
<Label>邮箱</Label>
<Input
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
placeholder="如:zhangsan@example.com"
/>
<Label>渠道类型 *</Label>
<Select value={channelType} onValueChange={setChannelType}>
<SelectTrigger>
<SelectValue placeholder="选择渠道类型" />
</SelectTrigger>
<SelectContent>
{CHANNEL_OPTIONS.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>钉钉(webhook URL)</Label>
<Label>Webhook URL *</Label>
<Input
value={form.dingtalk}
onChange={(e) => setForm({ ...form, dingtalk: e.target.value })}
placeholder="https://oapi.dingtalk.com/robot/send?access_token=..."
className="font-mono text-sm"
/>
</div>
<div className="space-y-2">
<Label>飞书(webhook URL)</Label>
<Input
value={form.feishu}
onChange={(e) => setForm({ ...form, feishu: e.target.value })}
placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..."
className="font-mono text-sm"
/>
</div>
<div className="space-y-2">
<Label>企业微信(webhook URL)</Label>
<Input
value={form.wechat}
onChange={(e) => setForm({ ...form, wechat: e.target.value })}
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
className="font-mono text-sm"
value={form.webhook_url}
onChange={(e) => setForm({ ...form, webhook_url: e.target.value })}
placeholder={selectedChannel?.placeholder || 'https://...'}
className="font-mono text-xs"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={!form.name.trim() || saving}>
<Button onClick={handleSave} disabled={!form.name.trim() || !form.webhook_url.trim() || saving}>
{saving ? '保存中...' : editingContact ? '保存修改' : '添加联系人'}
</Button>
</DialogFooter>