diff --git a/backend/internal/api/handlers/zabbix_datasource.go b/backend/internal/api/handlers/zabbix_datasource.go new file mode 100644 index 0000000..f22b4c2 --- /dev/null +++ b/backend/internal/api/handlers/zabbix_datasource.go @@ -0,0 +1,205 @@ +package handlers + +import ( + "context" + "net/http" + "time" + + "ezops/internal/models" + "ezops/pkg/zabbix" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ZabbixDSHandler Zabbix 数据源管理 handler // Logan.Li +type ZabbixDSHandler struct { + db *gorm.DB +} + +// NewZabbixDSHandler 创建 Zabbix 数据源 handler +func NewZabbixDSHandler(db *gorm.DB) *ZabbixDSHandler { + return &ZabbixDSHandler{db: db} +} + +// ============================================ +// CRUD +// ============================================ + +// List 数据源列表 +// GET /api/v1/zabbix-datasources +func (h *ZabbixDSHandler) List(c *gin.Context) { + var sources []models.ZabbixDataSource + if err := h.db.Order("is_default DESC, id ASC").Find(&sources).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "查询失败: " + err.Error()}) + return + } + if sources == nil { + sources = []models.ZabbixDataSource{} + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: sources}) +} + +// Get 获取单个数据源 +// GET /api/v1/zabbix-datasources/:id +func (h *ZabbixDSHandler) Get(c *gin.Context) { + id := c.Param("id") + var ds models.ZabbixDataSource + if err := h.db.First(&ds, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds}) +} + +// Create 创建数据源 +// POST /api/v1/zabbix-datasources +func (h *ZabbixDSHandler) Create(c *gin.Context) { + var req models.CreateZabbixDSReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + + if req.IsDefault { + h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false) + } + + ds := models.ZabbixDataSource{ + Name: req.Name, + URL: req.URL, + Username: req.Username, + Password: req.Password, + TrendsAfter: req.TrendsAfter, + TrendsRange: req.TrendsRange, + TrendsCacheTTL: req.TrendsCacheTTL, + TrendsTimeout: req.TrendsTimeout, + DirectDB: req.DirectDB, + DisableAcknowledges: req.DisableAcknowledges, + DisableDataAlignment: req.DisableDataAlignment, + IsDefault: req.IsDefault, + Status: 1, + } + + if ds.TrendsCacheTTL <= 0 { + ds.TrendsCacheTTL = 3600 + } + if ds.TrendsTimeout <= 0 { + ds.TrendsTimeout = 30 + } + + if err := h.db.Create(&ds).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "创建失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Data: ds, Message: "创建成功"}) +} + +// Update 更新数据源 +// PUT /api/v1/zabbix-datasources/:id +func (h *ZabbixDSHandler) Update(c *gin.Context) { + id := c.Param("id") + var ds models.ZabbixDataSource + if err := h.db.First(&ds, id).Error; err != nil { + c.JSON(http.StatusNotFound, models.Response{Code: 404, Message: "数据源不存在"}) + return + } + + var req models.UpdateZabbixDSReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + + updates := map[string]interface{}{} + if req.Name != nil { updates["name"] = *req.Name } + if req.URL != nil { updates["url"] = *req.URL } + if req.Username != nil { updates["username"] = *req.Username } + if req.Password != nil && *req.Password != "" { updates["password"] = *req.Password } + if req.TrendsAfter != nil { updates["trends_after"] = *req.TrendsAfter } + if req.TrendsRange != nil { updates["trends_range"] = *req.TrendsRange } + if req.TrendsCacheTTL != nil { updates["trends_cache_ttl"] = *req.TrendsCacheTTL } + if req.TrendsTimeout != nil { updates["trends_timeout"] = *req.TrendsTimeout } + if req.DirectDB != nil { updates["direct_db"] = *req.DirectDB } + if req.DisableAcknowledges != nil { updates["disable_acknowledges"] = *req.DisableAcknowledges } + if req.DisableDataAlignment != nil { updates["disable_data_alignment"] = *req.DisableDataAlignment } + if req.IsDefault != nil { + updates["is_default"] = *req.IsDefault + if *req.IsDefault { + h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ? AND id != ?", true, id).Update("is_default", false) + } + } + if req.Status != nil { updates["status"] = *req.Status } + + if len(updates) > 0 { + if err := h.db.Model(&ds).Updates(updates).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "更新失败: " + err.Error()}) + return + } + } + + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "更新成功"}) +} + +// Delete 删除数据源 +// DELETE /api/v1/zabbix-datasources/:id +func (h *ZabbixDSHandler) Delete(c *gin.Context) { + id := c.Param("id") + if err := h.db.Delete(&models.ZabbixDataSource{}, id).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"}) +} + +// SetDefault 设为默认 +// POST /api/v1/zabbix-datasources/:id/default +func (h *ZabbixDSHandler) SetDefault(c *gin.Context) { + id := c.Param("id") + h.db.Model(&models.ZabbixDataSource{}).Where("is_default = ?", true).Update("is_default", false) + if err := h.db.Model(&models.ZabbixDataSource{}).Where("id = ?", id).Update("is_default", true).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "设置失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已设为默认"}) +} + +// TestConnection 测试 Zabbix 连接 +// POST /api/v1/zabbix-datasources/test +func (h *ZabbixDSHandler) TestConnection(c *gin.Context) { + var req models.TestZabbixDSReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "参数错误: " + err.Error()}) + return + } + + client := zabbix.NewClient(req.URL, req.Username, req.Password, + zabbix.WithHTTPTimeout(10*time.Second), + ) + + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + + if err := client.Auth(ctx); err != nil { + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Data: map[string]interface{}{ + "success": false, + "message": "认证失败: " + err.Error(), + }, + }) + return + } + + token := client.GetAuthToken() + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Data: map[string]interface{}{ + "success": true, + "message": "连接成功,Zabbix 认证通过", + "session_id": token, + }, + }) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 4102144..80e1ba9 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -521,6 +521,19 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config, c promDSGroup.POST("/test", promDSHandler.TestConnection) } + // Zabbix 数据源管理 — CRUD + 连接测试 // Logan.Li + zabbixDSHandler := handlers.NewZabbixDSHandler(db) + zabbixDSGroup := authorized.Group("/zabbix-datasources") + { + zabbixDSGroup.GET("", zabbixDSHandler.List) + zabbixDSGroup.GET("/:id", zabbixDSHandler.Get) + zabbixDSGroup.POST("", zabbixDSHandler.Create) + zabbixDSGroup.PUT("/:id", zabbixDSHandler.Update) + zabbixDSGroup.DELETE("/:id", zabbixDSHandler.Delete) + zabbixDSGroup.POST("/:id/default", zabbixDSHandler.SetDefault) + zabbixDSGroup.POST("/test", zabbixDSHandler.TestConnection) + } + // 站点拨测 — HTTP/TCP/DNS/PING 主动监控 // Logan.Li siteHandler := handlers.NewSiteHandler(db) siteGroup := authorized.Group("/site") diff --git a/backend/internal/models/zabbix_datasource.go b/backend/internal/models/zabbix_datasource.go new file mode 100644 index 0000000..7929054 --- /dev/null +++ b/backend/internal/models/zabbix_datasource.go @@ -0,0 +1,82 @@ +package models + +import "time" + +// ZabbixDataSource Zabbix 数据源配置(参考 Grafana 界面设计) // Logan.Li +type ZabbixDataSource struct { + ID uint `gorm:"primaryKey;autoIncrement;comment:主键ID" json:"id"` + Name string `gorm:"type:varchar(128);not null;uniqueIndex;comment:数据源名称" json:"name"` + URL string `gorm:"type:varchar(512);not null;comment:Zabbix API地址" json:"url"` + Username string `gorm:"type:varchar(128);not null;comment:用户名" json:"username"` + Password string `gorm:"type:varchar(256);not null;comment:密码" json:"password,omitempty"` + + // Trends 设置 + TrendsAfter int `gorm:"default:0;comment:趋势数据起始时间(天前)" json:"trends_after"` + TrendsRange int `gorm:"default:0;comment:趋势数据范围(秒)" json:"trends_range"` + TrendsCacheTTL int `gorm:"default:3600;comment:趋势缓存TTL(秒)" json:"trends_cache_ttl"` + TrendsTimeout int `gorm:"default:30;comment:趋势查询超时(秒)" json:"trends_timeout"` + + // Direct DB Connection + DirectDB bool `gorm:"default:false;comment:启用直连数据库" json:"direct_db"` + + // Other + DisableAcknowledges bool `gorm:"default:false;comment:只读用户禁用确认" json:"disable_acknowledges"` + DisableDataAlignment bool `gorm:"default:false;comment:禁用数据对齐" json:"disable_data_alignment"` + + // 状态 + IsDefault bool `gorm:"default:false;index;comment:是否默认数据源" json:"is_default"` + Status int `gorm:"default:1;comment:状态: 1启用 0禁用" json:"status"` + + CreatedAt time.Time `gorm:"not null;index;comment:创建时间" json:"created_at"` + UpdatedAt time.Time `gorm:"not null;comment:更新时间" json:"updated_at"` + DeletedAt *time.Time `gorm:"index;comment:删除时间" json:"deleted_at,omitempty"` +} + +// TableName 指定表名 +func (ZabbixDataSource) TableName() string { + return "zabbix_datasources" +} + +// ============================================ +// 请求/响应 struct +// ============================================ + +// CreateZabbixDSReq 创建 Zabbix 数据源请求 +type CreateZabbixDSReq struct { + Name string `json:"name" binding:"required"` + URL string `json:"url" binding:"required"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` + TrendsAfter int `json:"trends_after"` + TrendsRange int `json:"trends_range"` + TrendsCacheTTL int `json:"trends_cache_ttl"` + TrendsTimeout int `json:"trends_timeout"` + DirectDB bool `json:"direct_db"` + DisableAcknowledges bool `json:"disable_acknowledges"` + DisableDataAlignment bool `json:"disable_data_alignment"` + IsDefault bool `json:"is_default"` +} + +// UpdateZabbixDSReq 更新 Zabbix 数据源请求 +type UpdateZabbixDSReq struct { + Name *string `json:"name"` + URL *string `json:"url"` + Username *string `json:"username"` + Password *string `json:"password"` + TrendsAfter *int `json:"trends_after"` + TrendsRange *int `json:"trends_range"` + TrendsCacheTTL *int `json:"trends_cache_ttl"` + TrendsTimeout *int `json:"trends_timeout"` + DirectDB *bool `json:"direct_db"` + DisableAcknowledges *bool `json:"disable_acknowledges"` + DisableDataAlignment *bool `json:"disable_data_alignment"` + IsDefault *bool `json:"is_default"` + Status *int `json:"status"` +} + +// TestZabbixDSReq 测试 Zabbix 连接请求 +type TestZabbixDSReq struct { + URL string `json:"url" binding:"required"` + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} diff --git a/backend/pkg/database/database.go b/backend/pkg/database/database.go index 71edb8e..b597704 100644 --- a/backend/pkg/database/database.go +++ b/backend/pkg/database/database.go @@ -131,6 +131,7 @@ func AutoMigrate(db *gorm.DB) error { &models.AlertSilence{}, // 告警静默 // Logan.Li &models.AlertEventV2{}, // 告警事件V2 // Logan.Li &models.PrometheusDataSource{}, // Prometheus 数据源配置 // Logan.Li + &models.ZabbixDataSource{}, // Zabbix 数据源配置 // Logan.Li } var lastErr error diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aea4323..c4e5215 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,7 +33,7 @@ import TraceManager from '@/components/monitor/trace/TraceManager'; import HostMonitorDashboard from '@/components/monitor/dashboard/HostMonitorDashboard'; import { MonitorCenter } from '@/pages/MonitorCenter'; import { MetricsExplorer } from '@/pages/MetricsExplorer'; -import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager'; +import DatasourcePage from '@/components/monitor/datasource/DatasourcePage'; const queryClient = new QueryClient({ defaultOptions: { @@ -75,7 +75,7 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/monitor/datasource/DatasourcePage.tsx b/frontend/src/components/monitor/datasource/DatasourcePage.tsx new file mode 100644 index 0000000..2c50441 --- /dev/null +++ b/frontend/src/components/monitor/datasource/DatasourcePage.tsx @@ -0,0 +1,47 @@ +// 数据源管理页面 — Prometheus + Zabbix 双 Tab // Logan.Li +import { useState } from 'react'; +import { Database } from 'lucide-react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import PrometheusDatasourceManager from '@/components/monitor/datasource/PrometheusDatasourceManager'; +import ZabbixDatasourceManager from '@/components/monitor/datasource/ZabbixDatasourceManager'; + +export default function DatasourcePage() { + const [activeTab, setActiveTab] = useState('prometheus'); + + return ( +
+
+ +

数据源管理

+
+ + + + + { (e.target as HTMLImageElement).style.display = 'none'; }} + /> + Prometheus + + + { (e.target as HTMLImageElement).style.display = 'none'; }} + /> + Zabbix + + + + + + + + + + +
+ ); +} diff --git a/frontend/src/components/monitor/datasource/ZabbixDatasourceManager.tsx b/frontend/src/components/monitor/datasource/ZabbixDatasourceManager.tsx new file mode 100644 index 0000000..c527434 --- /dev/null +++ b/frontend/src/components/monitor/datasource/ZabbixDatasourceManager.tsx @@ -0,0 +1,400 @@ +// Zabbix 数据源管理 — 参照 Grafana Zabbix 数据源配置界面 // Logan.Li +import { useState, useEffect, useCallback } from 'react'; +import { + Database, Plus, Trash2, Star, RefreshCw, Plug, + Shield, Globe, Settings, Key, ChevronDown, ChevronUp, + Edit, Clock, CheckCircle2, XCircle +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { + Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter +} from '@/components/ui/dialog'; +import { Separator } from '@/components/ui/separator'; +import { toast } from 'sonner'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { zabbixDatasourceApi } from '@/services/api'; + +interface ZabbixDS { + id: number; + name: string; + url: string; + username: string; + password: string; + trends_after: number; + trends_range: number; + trends_cache_ttl: number; + trends_timeout: number; + direct_db: boolean; + disable_acknowledges: boolean; + disable_data_alignment: boolean; + is_default: boolean; + status: number; + created_at: string; +} + +const defaultForm = { + name: '', url: '', username: '', password: '', + trends_after: 0, trends_range: 0, trends_cache_ttl: 3600, trends_timeout: 30, + direct_db: false, disable_acknowledges: false, disable_data_alignment: false, + is_default: false, +}; + +export default function ZabbixDatasourceManager() { + const _confirm = useConfirm(); + const [sources, setSources] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [form, setForm] = useState({ ...defaultForm }); + + const [showAuth, setShowAuth] = useState(false); + const [showTrends, setShowTrends] = useState(false); + const [showOther, setShowOther] = useState(false); + + const [testing, setTesting] = useState(false); + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); + + const fetchList = useCallback(async () => { + try { + const res: any = await zabbixDatasourceApi.list(); + setSources(res?.data || res || []); + } catch { + toast.error('获取数据源列表失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { fetchList(); }, [fetchList]); + + const openCreate = () => { + setEditingId(null); + setForm({ ...defaultForm }); + setTestResult(null); + setShowAuth(false); setShowTrends(false); setShowOther(false); + setDialogOpen(true); + }; + + const openEdit = (ds: ZabbixDS) => { + setEditingId(ds.id); + setForm({ + name: ds.name, url: ds.url, + username: ds.username || '', password: ds.password || '', + trends_after: ds.trends_after || 0, trends_range: ds.trends_range || 0, + trends_cache_ttl: ds.trends_cache_ttl || 3600, trends_timeout: ds.trends_timeout || 30, + direct_db: ds.direct_db || false, + disable_acknowledges: ds.disable_acknowledges || false, + disable_data_alignment: ds.disable_data_alignment || false, + is_default: ds.is_default || false, + }); + setTestResult(null); + setDialogOpen(true); + }; + + const updateForm = (field: string, value: any) => { + setForm(prev => ({ ...prev, [field]: value })); + }; + + const handleSave = async () => { + if (!form.name.trim()) { toast.error('请输入数据源名称'); return; } + if (!form.url.trim()) { toast.error('请输入 Zabbix API URL'); return; } + if (!form.username.trim()) { toast.error('请输入用户名'); return; } + if (!form.password.trim()) { toast.error('请输入密码'); return; } + + try { + if (editingId) { + await zabbixDatasourceApi.update(editingId, form); + toast.success('数据源已更新'); + } else { + await zabbixDatasourceApi.create(form); + toast.success('数据源已创建'); + } + setDialogOpen(false); + fetchList(); + } catch (err: any) { + toast.error(err?.message || '操作失败'); + } + }; + + const handleDelete = async (ds: ZabbixDS) => { + const ok = await _confirm({ + title: '删除数据源', + description: `确定删除「${ds.name}」?`, + variant: 'destructive', + }); + if (!ok) return; + try { + await zabbixDatasourceApi.delete(ds.id); + toast.success('已删除'); + fetchList(); + } catch { toast.error('删除失败'); } + }; + + const handleSetDefault = async (ds: ZabbixDS) => { + try { + await zabbixDatasourceApi.setDefault(ds.id); + toast.success(`已将「${ds.name}」设为默认`); + fetchList(); + } catch { toast.error('设置失败'); } + }; + + const handleTest = async () => { + if (!form.url.trim() || !form.username.trim()) { + toast.error('请填写 URL 和用户名'); + return; + } + setTesting(true); + setTestResult(null); + try { + const res: any = await zabbixDatasourceApi.testConnection({ + url: form.url.trim(), + username: form.username.trim(), + password: form.password || '', + }); + const data = res?.data || res; + setTestResult(data); + } catch (err: any) { + setTestResult({ success: false, message: err?.message || '测试失败' }); + } finally { + setTesting(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

+ { (e.target as HTMLImageElement).style.display = 'none'; }} + /> + Zabbix 数据源 +

+

配置 Zabbix API 连接,获取主机监控数据

+
+ +
+ + {sources.length === 0 ? ( + + + +

暂无 Zabbix 数据源

+ +
+
+ ) : ( +
+ {sources.map(ds => ( + + +
+
+
+
+ {ds.name} + {ds.is_default && 默认} + {ds.status === 1 + ? 启用 + : 禁用} +
+
+ {ds.url} + 用户: {ds.username} + {ds.direct_db && 直连DB} +
+
+
+ {!ds.is_default && ( + + )} + + +
+
+ + + ))} +
+ )} + + {/* 编辑 Dialog — 参照 Grafana Zabbix 数据源配置 */} + + + + + + {editingId ? '编辑 Zabbix 数据源' : '新建 Zabbix 数据源'} + + + 配置 Zabbix API 连接,用于主机监控与告警数据获取 + + + +
+ {/* Name + Default */} +
+
+ + updateForm('name', e.target.value)} /> +
+
+
+ updateForm('is_default', v)} /> + +
+
+
+ + {/* HTTP + Auth */} +
+
+ HTTP +
+
+ + updateForm('url', e.target.value)} /> +
+
+ + {/* API Details — 折叠 */} +
+ + {showAuth && ( +
+ updateForm('username', e.target.value)} /> + updateForm('password', e.target.value)} /> +
+ )} +
+ + {/* Trends — 折叠 */} +
+ + {showTrends && ( +
+
+
+ + updateForm('trends_after', parseInt(e.target.value) || 0)} /> +
+
+ + updateForm('trends_range', parseInt(e.target.value) || 0)} /> +
+
+
+
+ + updateForm('trends_cache_ttl', parseInt(e.target.value) || 3600)} /> +
+
+ + updateForm('trends_timeout', parseInt(e.target.value) || 30)} /> +
+
+
+ )} +
+ + {/* 其他设置 — 折叠 */} +
+ + {showOther && ( +
+
+ + updateForm('direct_db', v)} /> +
+
+ + updateForm('disable_acknowledges', v)} /> +
+
+ + updateForm('disable_data_alignment', v)} /> +
+
+ )} +
+ + {/* 测试结果 */} + {testResult && ( +
+ {testResult.success ? : } + {testResult.message} +
+ )} +
+ + + +
+ + + + +
+
+ ); +} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index df12cb9..bb52d08 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -695,6 +695,17 @@ export const promDatasourceApi = { testConnection: (data: any) => http.post('/prometheus-datasources/test', data), }; +// ==================== Zabbix 数据源管理 API ==================== // Logan.Li +export const zabbixDatasourceApi = { + list: () => http.get('/zabbix-datasources'), + get: (id: number | string) => http.get(`/zabbix-datasources/${id}`), + create: (data: any) => http.post('/zabbix-datasources', data), + update: (id: number | string, data: any) => http.put(`/zabbix-datasources/${id}`, data), + delete: (id: number | string) => http.delete(`/zabbix-datasources/${id}`), + setDefault: (id: number | string) => http.post(`/zabbix-datasources/${id}/default`), + testConnection: (data: any) => http.post('/zabbix-datasources/test', data), +}; + // AI 智能助手 API(后端 LLM 配置 + 对话代理,OpenAI 兼容) // Logan.Li export interface LLMConfigData { configured?: boolean;