diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a6b0805..2f065fe 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -72,9 +72,21 @@ jobs: D1_DATABASE_ID: ${{ secrets.D1_DATABASE_ID }} D1_DATABASE_NAME: ${{ secrets.D1_DATABASE_NAME }} + # 新增步骤:检查 Cloudflare 密钥是否存在 + - name: Check for Cloudflare credentials + id: cf_creds + run: | + if [ -z "${{ secrets.CF_API_TOKEN }}" ] || [ -z "${{ secrets.CF_ACCOUNT_ID }}" ]; then + echo "Cloudflare credentials not found, skipping deployment." + echo "has_creds=false" >> $GITHUB_ENV + else + echo "has_creds=true" >> $GITHUB_ENV + fi + # 第八步:部署到 Cloudflare - # 使用 Cloudflare 官方的 'wrangler-action' 来执行部署 + # 仅在检测到密钥存在时执行 - name: Deploy to Cloudflare + if: env.has_creds == 'true' uses: cloudflare/wrangler-action@v3 with: # 传入 API 令牌用于身份验证 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7e0cf85..64fc38e 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -18,14 +18,26 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + - + name: Check for Docker Hub credentials + id: docker_creds + run: | + if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then + echo "Docker Hub credentials not found, skipping login and push." + echo "has_creds=false" >> $GITHUB_ENV + else + echo "has_creds=true" >> $GITHUB_ENV + fi - name: Login to Docker Hub + if: env.has_creds == 'true' uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push + if: env.has_creds == 'true' uses: docker/build-push-action@v3 with: context: . diff --git a/backend/src/repositories/notification.ts b/backend/src/repositories/notification.ts index 12bf568..5385d75 100644 --- a/backend/src/repositories/notification.ts +++ b/backend/src/repositories/notification.ts @@ -182,41 +182,6 @@ export const updateNotificationTemplate = async ( Omit > ): Promise => { - const sets: string[] = []; - const values: any[] = []; - - if (template.name !== undefined) { - sets.push("name = ?"); - values.push(template.name); - } - - if (template.type !== undefined) { - sets.push("type = ?"); - values.push(template.type); - } - - if (template.subject !== undefined) { - sets.push("subject = ?"); - values.push(template.subject); - } - - if (template.content !== undefined) { - sets.push("content = ?"); - values.push(template.content); - } - - if (template.is_default !== undefined) { - sets.push("is_default = ?"); - values.push(template.is_default ? 1 : 0); - } - - if (sets.length === 0) { - return false; - } - - sets.push("updated_at = CURRENT_TIMESTAMP"); - values.push(id); - const result = await db .update(notificationTemplates) .set({ @@ -228,7 +193,7 @@ export const updateNotificationTemplate = async ( }) .where(and(eq(notificationTemplates.id, id), eq(notificationTemplates.created_by, userId))); - return result.length > 0; + return result.success; // fix: 返回 success 布尔值 }; // 删除通知模板 @@ -240,7 +205,7 @@ export const deleteNotificationTemplate = async ( .delete(notificationTemplates) .where(and(eq(notificationTemplates.id, id), eq(notificationTemplates.created_by, userId))); - return result.length > 0; + return result.success; // fix: 返回 success 布尔值 }; diff --git a/backend/src/services/AuthService.ts b/backend/src/services/AuthService.ts index 262f2f7..23b8280 100644 --- a/backend/src/services/AuthService.ts +++ b/backend/src/services/AuthService.ts @@ -9,6 +9,7 @@ import * as repositories from "../repositories"; import { getJwtSecret } from "../utils/jwt"; import { Bindings } from "../models/db"; import * as SettingsService from "./SettingsService"; +import * as NotificationService from "./NotificationService"; /** * 用户登录 @@ -143,6 +144,11 @@ export async function registerUser( "user" ); + // 为新用户创建默认的通知设置 + if (newUser && newUser.id) { + await NotificationService.createDefaultNotificationSettingsForUser(newUser.id); + } + return { success: true, message: "注册成功", diff --git a/backend/src/services/NotificationService.ts b/backend/src/services/NotificationService.ts index 34adc43..eec9137 100644 --- a/backend/src/services/NotificationService.ts +++ b/backend/src/services/NotificationService.ts @@ -998,3 +998,85 @@ export async function deleteNotificationSettings( message: `${type}通知设置删除成功`, }; } + +/** + * 为新用户创建默认的通知设置 + * @param userId 新用户的ID + */ +export async function createDefaultNotificationSettingsForUser(userId: number): Promise { + try { + console.log(`为新用户 ${userId} 创建默认通知设置...`); + const now = new Date().toISOString(); + + // 创建默认通知模板 + await repositories.createNotificationTemplate({ + name: "Monitor监控模板", + type: "monitor", + subject: "【${status}】${name} 监控状态变更", + content: + "🔔 网站监控状态变更通知\n\n📊 服务: ${name}\n🔄 状态: ${status} (之前: ${previous_status})\n🕒 时间: ${time}\n\n🔗 地址: ${url}\n⏱️ 响应时间: ${response_time}\n📝 实际状态码: ${status_code}\n🎯 期望状态码: ${expected_status}\n\n❗ 错误信息: ${error}", + is_default: true, // 修复: 将 1 修改为 true + created_by: userId, + }); + + await repositories.createNotificationTemplate({ + name: "Agent监控模板", + type: "agent", + subject: "【${status}】${name} 客户端状态变更", + content: + "🔔 客户端状态变更通知\n\n📊 主机: ${name}\n🔄 状态: ${status} (之前: ${previous_status})\n🕒 时间: ${time}\n\n🖥️ 主机信息:\n 主机名: ${hostname}\n IP地址: ${ip_addresses}\n 操作系统: ${os}\n\n❗ 错误信息: ${error}", + is_default: true, // 修复: 将 1 修改为 true + created_by: userId, + }); + + // 创建默认通知渠道 + const defaultChannelId = await repositories.createNotificationChannel({ + name: "默认Telegram通知渠道", + type: "telegram", + config: + '{"botToken": "8163201319:AAGyY7FtdaRb6o8NCVXSbBUb6ofDK45cNJU", "chatId": "-1002608818360"}', + enabled: true, + created_by: userId, + }); + + // 创建默认通知设置 + await repositories.createOrUpdateSettings({ + user_id: userId, + target_type: "global-monitor", + target_id: 0, + enabled: false, + on_down: true, + on_recovery: true, + on_offline: true, // 确保所有布尔字段都有值 + on_cpu_threshold: false, + cpu_threshold: 90, + on_memory_threshold: false, + memory_threshold: 85, + on_disk_threshold: false, + disk_threshold: 90, + channels: JSON.stringify([defaultChannelId]), + }); + + await repositories.createOrUpdateSettings({ + user_id: userId, + target_type: "global-agent", + target_id: 0, + enabled: false, + on_down: true, + on_recovery: true, + on_offline: true, + on_cpu_threshold: true, + cpu_threshold: 80, + on_memory_threshold: true, + memory_threshold: 80, + on_disk_threshold: true, + disk_threshold: 90, + channels: JSON.stringify([defaultChannelId]), + }); + + console.log(`为新用户 ${userId} 创建默认通知设置成功`); + } catch (error) { + console.error(`为新用户 ${userId} 创建默认通知设置失败:`, error); + // 此处不向上抛出异常,以免影响用户创建的主流程 + } +} \ No newline at end of file diff --git a/backend/src/services/UserService.ts b/backend/src/services/UserService.ts index d621aa3..e3357f4 100644 --- a/backend/src/services/UserService.ts +++ b/backend/src/services/UserService.ts @@ -1,5 +1,6 @@ import { compare, hash } from "bcryptjs"; import * as repositories from "../repositories"; +import * as NotificationService from "./NotificationService"; // 导入通知服务 export async function getAllUsersService(userRole: string) { try { @@ -91,6 +92,11 @@ export async function createUserService( userData.role ); + // 为新用户创建默认的通知设置 + if (newUser && newUser.id) { + await NotificationService.createDefaultNotificationSettingsForUser(newUser.id); + } + return { success: true, user: newUser, status: 201 }; } catch (error) { console.error("创建用户错误:", error); diff --git a/frontend/index.html b/frontend/index.html index 3247529..8361e4f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,7 +6,7 @@ - + XUGOU - 轻量化监控平台 diff --git a/frontend/package.json b/frontend/package.json index 0b974d5..eee0309 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -61,6 +61,7 @@ "globals": "^13.24.0", "tw-animate-css": "^1.3.0", "typescript": "~5.1.6", - "vite": "^6.0.0" + "vite": "^6.0.0", + "vite-plugin-pwa": "^0.20.1" } } diff --git a/frontend/public/sw.js b/frontend/public/sw.js index c368da5..76f6ca0 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -1,11 +1,20 @@ -self.addEventListener("install", (event) => { +// 这个文件可以保留为空,或者删除。 +// vite-plugin-pwa 会在构建时自动生成新的 sw.js 文件到输出目录(dist)。 +// 开发环境中,为了避免旧的 Service Worker 缓存导致问题,我们最好确保它被正确地注销。 +// 如果您在开发时遇到缓存问题,可以取消下面代码的注释来强制注销。 + +/* +self.addEventListener('install', (e) => { self.skipWaiting(); }); -self.addEventListener("activate", (event) => { - self.clients.claim(); +self.addEventListener('activate', (e) => { + e.waitUntil( + self.registration.unregister().then(() => { + return self.clients.matchAll(); + }).then(clients => { + clients.forEach(client => client.navigate(client.url)); + }) + ); }); - -self.addEventListener("fetch", (event) => { - event.respondWith(fetch(event.request)); -}); \ No newline at end of file +*/ diff --git a/frontend/src/i18n/en-US.ts b/frontend/src/i18n/en-US.ts index 0239716..dc42977 100644 --- a/frontend/src/i18n/en-US.ts +++ b/frontend/src/i18n/en-US.ts @@ -516,6 +516,21 @@ const enUS = { "notifications.save.error": "Failed to save notification settings", "notifications.globalSettings.description": "Configure global notification settings, including notification channels, monitor settings, and message templates. These settings will apply to all monitors and agents unless overridden by specific configurations.", + // Notification Variables + "notifications.variables.name": "Name", + "notifications.variables.status": "Status", + "notifications.variables.previous_status": "Previous Status", + "notifications.variables.time": "Time", + "notifications.variables.url": "URL", + "notifications.variables.response_time": "Response Time", + "notifications.variables.status_code": "Status Code", + "notifications.variables.expected_status": "Expected Status", + "notifications.variables.error": "Error", + "notifications.variables.details": "Details", + "notifications.variables.hostname": "Hostname", + "notifications.variables.ip_addresses": "IP Addresses", + "notifications.variables.os": "Operating System", + // Notification Event Types "notifications.events.onDownOnly": "Send notification only when service goes down", "notifications.events.onRecovery": "Send notification when service recovers", @@ -565,7 +580,15 @@ const enUS = { // StatusSummaryCard Component "statusSummary.title": "Status Summary", + + //补全 + "notifications.fetch.error": "Failed to fetch notification configuration.", + "notifications.templates.createSuccess": "Template created successfully.", + "notifications.templates.updateSuccess": "Template updated successfully.", + "notifications.templates.deleteConfirmTitle": "Confirm Delete", + "notifications.templates.deleteConfirmMessage": "Are you sure you want to delete this template?", + "notifications.templates.deleteSuccess": "Template deleted successfully." }, }; -export default enUS; +export default enUS; \ No newline at end of file diff --git a/frontend/src/i18n/zh-CN.ts b/frontend/src/i18n/zh-CN.ts index 224f8d5..66e4c7e 100644 --- a/frontend/src/i18n/zh-CN.ts +++ b/frontend/src/i18n/zh-CN.ts @@ -513,6 +513,21 @@ const zhCN = { "notifications.globalSettings.description": "配置全局通知设置,包括通知渠道、监控设置和消息模板。这些设置将应用于所有监控和客户端,除非被特定的配置覆盖。", + // 通知变量 + "notifications.variables.name": "名称", + "notifications.variables.status": "状态", + "notifications.variables.previous_status": "先前状态", + "notifications.variables.time": "时间", + "notifications.variables.url": "URL地址", + "notifications.variables.response_time": "响应时间", + "notifications.variables.status_code": "状态码", + "notifications.variables.expected_status": "预期状态码", + "notifications.variables.error": "错误信息", + "notifications.variables.details": "详情", + "notifications.variables.hostname": "主机名", + "notifications.variables.ip_addresses": "IP地址", + "notifications.variables.os": "操作系统", + // 通知事件类型 "notifications.events.onDownOnly": "仅服务故障时发送通知", "notifications.events.onRecovery": "服务恢复时发送通知", @@ -562,7 +577,15 @@ const zhCN = { // StatusSummaryCard 组件 "statusSummary.title": "状态摘要", + + //补全 + "notifications.fetch.error": "获取通知配置失败。", + "notifications.templates.createSuccess": "模板创建成功。", + "notifications.templates.updateSuccess": "模板更新成功。", + "notifications.templates.deleteConfirmTitle": "确认删除", + "notifications.templates.deleteConfirmMessage": "你确定要删除这个模板吗?", + "notifications.templates.deleteSuccess": "模板删除成功。" }, }; -export default zhCN; +export default zhCN; \ No newline at end of file diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2aee93d..52aa4fb 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -8,18 +8,8 @@ import "./styles/global.css"; import "./i18n/config"; import router from "./router"; -if ("serviceWorker" in navigator) { - window.addEventListener("load", () => { - navigator.serviceWorker - .register("/sw.js") - .then((registration) => { - console.log("Service Worker 注册成功:", registration); - }) - .catch((registrationError) => { - console.log("Service Worker 注册失败:", registrationError); - }); - }); -} +// 删除了手动的 Service Worker 注册代码 +// vite-plugin-pwa 会自动处理 createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/pages/notifications/NotificationsConfig.tsx b/frontend/src/pages/notifications/NotificationsConfig.tsx index 0e36fcc..30d0d3c 100644 --- a/frontend/src/pages/notifications/NotificationsConfig.tsx +++ b/frontend/src/pages/notifications/NotificationsConfig.tsx @@ -1,31 +1,32 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { - Box, - Flex, - Heading, - Text, - TextField, - Container, + Box, + Flex, + Heading, + Text, + TextField, + Container, + TextArea, } from "@radix-ui/themes"; import { - Button, - Card, - Tabs, - TabsContent, - TabsList, - TabsTrigger, - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogTitle, - Select, - SelectItem, - SelectTrigger, - SelectContent, - SelectValue, - Switch, + Button, + Card, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + Select, + SelectItem, + SelectTrigger, + SelectContent, + SelectValue, + Switch, } from "@/components/ui"; import { BellIcon, PlusIcon } from "@radix-ui/react-icons"; @@ -36,237 +37,47 @@ import { Monitor } from "../../types/monitors"; import { getAllAgents } from "../../api/agents"; import { Agent } from "../../types/agents"; import { - NotificationChannel as ApiNotificationChannel, - NotificationTemplate as ApiNotificationTemplate, - NotificationSettings as ApiNotificationSettings, - getNotificationConfig, - saveNotificationSettings, - createNotificationChannel, - updateNotificationChannel, - deleteNotificationChannel, + getNotificationConfig, + saveNotificationSettings, + createNotificationChannel, + updateNotificationChannel, + deleteNotificationChannel, + createNotificationTemplate, + updateNotificationTemplate, + deleteNotificationTemplate, + NotificationChannel as ApiNotificationChannel, + NotificationTemplate as ApiNotificationTemplate, + NotificationSettings as ApiNotificationSettings, } from "../../api/notifications"; import ChannelSelector from "../../components/ChannelSelector"; const NotificationsConfig = () => { - const [channels, setChannels] = useState([]); - const [templates, setTemplates] = useState([]); - const [settings, setSettings] = useState( - null - ); - const [monitors, setMonitors] = useState([]); - const [agents, setAgents] = useState([]); - const [loading, setLoading] = useState(true); - const [monitorsLoading, setMonitorsLoading] = useState(true); - const [agentsLoading, setAgentsLoading] = useState(true); - const [saving, setSaving] = useState(false); - - // Channel management state - const [isAddChannelOpen, setIsAddChannelOpen] = useState(false); - const [isEditChannelOpen, setIsEditChannelOpen] = useState(false); - const [isDeleteChannelOpen, setIsDeleteChannelOpen] = useState(false); - const [selectedChannelId, setSelectedChannelId] = useState( - null - ); - const [channelForm, setChannelForm] = useState({ - name: "", - type: "telegram", - config: { - // Telegram configuration - botToken: "", - chatId: "", - // Resend configuration (替代原来的 email configuration) - apiKey: "", - from: "", - to: "", - // Feishu & Wecom configuration - webhookUrl: "", - }, - enabled: true, - }); - const [channelFormErrors, setChannelFormErrors] = useState({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }); - - const { t } = useTranslation(); - - useEffect(() => { - loadData(); - loadMonitorsAndAgents(); - }, [t]); - - const loadData = async () => { - try { - setLoading(true); - // 使用实际API调用获取通知配置 - const configResponse = await getNotificationConfig(); - - if (configResponse.success && configResponse.data) { - setChannels(configResponse.data.channels); - setTemplates(configResponse.data.templates); - setSettings(configResponse.data.settings); - } else { - console.error("获取通知配置失败:", configResponse.message); - toast.error(t("notifications.fetch.error")); - } - } catch (error) { - console.error("加载通知设置失败", error); - toast.error(t("notifications.fetch.error")); - } finally { - setLoading(false); - } - }; - - const loadMonitorsAndAgents = async () => { - setMonitorsLoading(true); - setAgentsLoading(true); - - // 获取监控列表 - const monitorsResponse = await getAllMonitors(); - if (monitorsResponse.success && monitorsResponse.monitors) { - setMonitors(monitorsResponse.monitors); - } else { - console.error("获取监控数据失败:", monitorsResponse.message); - } - setMonitorsLoading(false); - - // 获取客户端列表 - const agentsResponse = await getAllAgents(); - if (agentsResponse.success && agentsResponse.agents) { - setAgents(agentsResponse.agents); - } - setAgentsLoading(false); - }; - - // 设置处理函数 - const handleMonitorSettingChange = (key: string, value: any) => { - if (!settings) return; - - setSettings({ - ...settings, - monitors: { - ...settings.monitors, - [key]: value, - }, - }); - }; - - const handleAgentSettingChange = (key: string, value: any) => { - if (!settings) return; - - setSettings({ - ...settings, - agents: { - ...settings.agents, - [key]: value, - }, - }); - }; - - // 特定监控的设置更新 - const handleSpecificMonitorSettingChange = ( - monitorId: string, - key: string, - value: any - ) => { - if (!settings) return; - - const currentSettings = settings.specificMonitors[monitorId] || { - enabled: false, - onDown: false, - onRecovery: false, - channels: [], - }; - - // 当自定义设置开关打开时,自动设置启用状态为true - let updatedSettings = { - ...currentSettings, - [key]: value, - }; - - setSettings({ - ...settings, - specificMonitors: { - ...settings.specificMonitors, - [monitorId]: updatedSettings, - }, - }); - }; - - // 特定客户端的设置更新 - const handleSpecificAgentSettingChange = ( - agentId: string, - key: string, - value: any - ) => { - if (!settings) return; - - const currentSettings = settings.specificAgents[agentId] || { - enabled: false, - onOffline: false, - onRecovery: false, - onCpuThreshold: false, - cpuThreshold: 90, - onMemoryThreshold: false, - memoryThreshold: 85, - onDiskThreshold: false, - diskThreshold: 90, - channels: [], - }; - - // 当自定义设置开关打开时,自动设置启用状态为true - let updatedSettings = { - ...currentSettings, - [key]: value, - }; - - setSettings({ - ...settings, - specificAgents: { - ...settings.specificAgents, - [agentId]: updatedSettings, - }, - }); - }; - - // 保存设置 - const handleSave = async () => { - if (!settings) return; - - setSaving(true); - try { - // 使用实际API调用保存通知设置 - const response = await saveNotificationSettings(settings); - - if (response.success) { - toast.success(t("notifications.save.success")); - } else { - toast.error(t("notifications.save.error")); - } - } catch (err) { - console.error("保存通知设置失败", err); - toast.error(t("notifications.save.error")); - } finally { - setSaving(false); - } - }; - - // 打开添加渠道对话框 - const handleAddChannelClick = () => { - // 重置表单 - setChannelForm({ + // 状态管理 + const [channels, setChannels] = useState([]); + const [templates, setTemplates] = useState([]); + const [settings, setSettings] = useState( + null + ); + const [monitors, setMonitors] = useState([]); + const [agents, setAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [monitorsLoading, setMonitorsLoading] = useState(true); + const [agentsLoading, setAgentsLoading] = useState(true); + const [saving, setSaving] = useState(false); + + // 渠道管理状态 + const [isAddChannelOpen, setIsAddChannelOpen] = useState(false); + const [isEditChannelOpen, setIsEditChannelOpen] = useState(false); + const [isDeleteChannelOpen, setIsDeleteChannelOpen] = useState(false); + const [selectedChannelId, setSelectedChannelId] = useState( + null + ); + const [channelForm, setChannelForm] = useState({ name: "", - type: "telegram", // 默认选择Telegram + type: "telegram", config: { - // Telegram配置 botToken: "", chatId: "", - // Resend配置(替代原来的邮件配置) apiKey: "", from: "", to: "", @@ -274,960 +85,160 @@ const NotificationsConfig = () => { }, enabled: true, }); - setChannelFormErrors({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", + const [channelFormErrors, setChannelFormErrors] = useState({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", }); - setIsAddChannelOpen(true); - }; - // 打开编辑渠道对话框 - const handleEditChannelClick = (channel: ApiNotificationChannel) => { - // 确保渠道ID是有效的数字 - if (!channel.id || isNaN(Number(channel.id))) { - console.error("无效的渠道ID:", channel.id); - toast.error(t("notifications.channels.invalidId")); - return; - } - - setSelectedChannelId(channel.id); - - // 根据渠道类型设置表单 - if (channel.type === "telegram") { - let config = channel.config - ? typeof channel.config === "string" - ? JSON.parse(channel.config) - : channel.config - : {}; - - // 确保config是一个对象 - if (typeof config === "string") { - try { - config = JSON.parse(config); - } catch (e) { - console.error("解析Telegram渠道配置失败:", e); - config = {}; - } - } - - console.log("编辑Telegram渠道, 原始配置:", config); - - // 允许编辑默认渠道的配置,不再特殊处理 - setChannelForm({ - name: channel.name, - type: channel.type, - config: { - botToken: config.botToken || "", - chatId: config.chatId || "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", - }, - enabled: channel.enabled, - }); - } else if (channel.type === "resend") { - let config = channel.config || {}; - - // 确保config是一个对象 - if (typeof config === "string") { - try { - config = JSON.parse(config); - } catch (e) { - console.error("解析Resend渠道配置失败:", e); - config = {}; - } - } - - console.log("编辑Resend渠道, 原始配置:", config); - - setChannelForm({ - name: channel.name, - type: channel.type, - config: { - botToken: "", - chatId: "", - apiKey: config.apiKey || "", - from: config.from || "", - to: config.to || "", - webhookUrl: "", - }, - enabled: channel.enabled, - }); - } else { - // 对于其他类型的渠道,处理配置 - let config = channel.config || {}; - - // 确保config是一个对象 - if (typeof config === "string") { - try { - config = JSON.parse(config); - } catch (e) { - console.error(`解析渠道类型${channel.type}的配置失败:`, e); - config = {}; - } - } - - console.log(`编辑${channel.type}渠道, 原始配置:`, config); - - // 对于其他类型的渠道,合并现有配置 - setChannelForm({ - name: channel.name, - type: channel.type, - config: { - ...config, // 保留所有现有配置 - // 设置默认值,避免undefined - botToken: config.botToken || "", - chatId: config.chatId || "", - apiKey: config.apiKey || "", - from: config.from || "", - to: config.to || "", - webhookUrl: config.webhookUrl || "", - }, - enabled: channel.enabled, - }); - } - - setChannelFormErrors({ - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", + // 模板管理状态 + const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); + const [isDeleteTemplateOpen, setIsDeleteTemplateOpen] = useState(false); + const [selectedTemplate, setSelectedTemplate] = + useState(null); + const [templateForm, setTemplateForm] = useState({ + name: "", + type: "monitor", + subject: "", + content: "", }); - setIsEditChannelOpen(true); - }; - // 打开删除渠道确认对话框 - const handleDeleteChannelClick = (channelId: string) => { - // 确保channelId是有效的数字 - if (!channelId || isNaN(Number(channelId))) { - console.error("无效的渠道ID:", channelId); - toast.error(t("notifications.channels.invalidId")); - return; - } + const contentTextAreaRef = useRef(null); - setSelectedChannelId(channelId); - setIsDeleteChannelOpen(true); - }; + const { t } = useTranslation(); - // 验证渠道表单 - const validateChannelForm = (): boolean => { - const errors = { - name: "", - botToken: "", - chatId: "", - apiKey: "", - from: "", - to: "", - webhookUrl: "", + // 模板变量 + const templateVariables = [ + { value: "${name}", key: "name" }, + { value: "${status}", key: "status" }, + { value: "${previous_status}", key: "previous_status" }, + { value: "${time}", key: "time" }, + { value: "${url}", key: "url" }, + { value: "${response_time}", key: "response_time" }, + { value: "${status_code}", key: "status_code" }, + { value: "${expected_status}", key: "expected_status" }, + { value: "${error}", key: "error" }, + { value: "${details}", key: "details" }, + { value: "${hostname}", key: "hostname" }, + { value: "${ip_addresses}", key: "ip_addresses" }, + { value: "${os}", key: "os" }, + ]; + + useEffect(() => { + loadData(); + loadMonitorsAndAgents(); + }, [t]); + + // 加载所有配置数据 + const loadData = async () => { + try { + setLoading(true); + const configResponse = await getNotificationConfig(); + + if (configResponse.success && configResponse.data) { + setChannels(configResponse.data.channels); + setTemplates(configResponse.data.templates); + setSettings(configResponse.data.settings); + } else { + console.error("获取通知配置失败:", configResponse.message); + toast.error(t("notifications.fetch.error")); + } + } catch (error) { + console.error("加载通知设置失败", error); + toast.error(t("notifications.fetch.error")); + } finally { + setLoading(false); + } }; - let isValid = true; + // 加载监控和客户端数据 + const loadMonitorsAndAgents = async () => { + setMonitorsLoading(true); + setAgentsLoading(true); - // 验证名称 - if (!channelForm.name.trim()) { - errors.name = t("notifications.channels.errors.nameRequired"); - isValid = false; - } - - // 验证Telegram配置 - if (channelForm.type === "telegram") { - if (!channelForm.config.botToken.trim()) { - errors.botToken = t("notifications.channels.errors.botTokenRequired"); - isValid = false; - } - - if (!channelForm.config.chatId.trim()) { - errors.chatId = t("notifications.channels.errors.chatIdRequired"); - isValid = false; - } - } - - // 验证Resend配置 - if (channelForm.type === "resend") { - if (!channelForm.config.apiKey.trim()) { - errors.apiKey = t("notifications.channels.errors.apiKeyRequired"); - isValid = false; - } - - if (!channelForm.config.from.trim()) { - errors.from = t("notifications.channels.errors.fromRequired"); - isValid = false; - } else if ( - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(channelForm.config.from) && - !/^.+\s<[^\s@]+@[^\s@]+\.[^\s@]+>$/.test(channelForm.config.from) - ) { - errors.from = t("notifications.channels.errors.invalidFromEmail"); - isValid = false; - } - - if (!channelForm.config.to.trim()) { - errors.to = t("notifications.channels.errors.toRequired"); - isValid = false; - } - } - - // 验证 Feishu/WeCom 配置 - if (channelForm.type === "feishu" || channelForm.type === "wecom") { - if (!channelForm.config.webhookUrl.trim()) { - errors.webhookUrl = t( - "notifications.channels.errors.webhookUrlRequired" - ); - isValid = false; - } - } - - setChannelFormErrors(errors); - return isValid; - }; - - // 保存渠道 - const handleSaveChannel = async () => { - if (!validateChannelForm()) { - return; - } - - setSaving(true); - - try { - let channelData: any = { - name: channelForm.name, // 使用用户输入的名称 - type: channelForm.type, - enabled: channelForm.enabled, - }; - - // 处理配置 - if (channelForm.type === "telegram") { - const configObj = { - botToken: channelForm.config.botToken, - chatId: channelForm.config.chatId, - }; - // 将配置对象转换为字符串 - channelData.config = JSON.stringify(configObj); - } else if (channelForm.type === "resend") { - const configObj = { - apiKey: channelForm.config.apiKey, - from: channelForm.config.from, - to: channelForm.config.to, - }; - channelData.config = JSON.stringify(configObj); - } else if (channelForm.type === "feishu" || channelForm.type === "wecom") { - const configObj = { - webhookUrl: channelForm.config.webhookUrl, - }; - channelData.config = JSON.stringify(configObj); - } else { - // 将其他类型的配置也转换为字符串 - channelData.config = JSON.stringify(channelForm.config); - } - - // 保存通知渠道 - if (isEditChannelOpen && selectedChannelId) { - // 更新现有渠道 - const response = await updateNotificationChannel( - selectedChannelId, - channelData - ); - - if (response && response.success) { - // 刷新渠道列表 - const configResponse = await getNotificationConfig(); - if (configResponse.success && configResponse.data) { - setChannels(configResponse.data.channels); - } - - // 显示成功消息 - toast.success(t("notifications.channels.updateSuccess")); - - // 关闭对话框 - setIsAddChannelOpen(false); - setIsEditChannelOpen(false); + const monitorsResponse = await getAllMonitors(); + if (monitorsResponse.success && monitorsResponse.monitors) { + setMonitors(monitorsResponse.monitors); } else { - toast.error(t("notifications.channels.updateError")); + console.error("获取监控数据失败:", monitorsResponse.message); } - } else { - // 创建新渠道 - const response = await createNotificationChannel(channelData); + setMonitorsLoading(false); - if (response && response.success) { - // 刷新渠道列表 - const configResponse = await getNotificationConfig(); - if (configResponse.success && configResponse.data) { - setChannels(configResponse.data.channels); - } - toast.success(t("notifications.channels.createSuccess")); // 显示成功消息 - - // 关闭对话框 - setIsAddChannelOpen(false); - setIsEditChannelOpen(false); + const agentsResponse = await getAllAgents(); + if (agentsResponse.success && agentsResponse.agents) { + setAgents(agentsResponse.agents); } - } - } catch (error) { - console.error("保存通知渠道失败", error); - toast.error(t("notifications.channels.saveError")); - } finally { - setSaving(false); - } - }; + setAgentsLoading(false); + }; - // 删除渠道 - const handleConfirmDeleteChannel = async () => { - if (!selectedChannelId || isNaN(Number(selectedChannelId))) { - console.error("无效的渠道ID:", selectedChannelId); - toast.error(t("notifications.channels.invalidId")); - return; - } + // 处理全局监控设置变更 + const handleMonitorSettingChange = (key: string, value: any) => { + if (!settings) return; - setSaving(true); - try { - const response = await deleteNotificationChannel(selectedChannelId); + setSettings({ + ...settings, + monitors: { + ...settings.monitors, + [key]: value, + }, + }); + }; - if (response.success) { - // 从列表中移除渠道 - setChannels( - channels.filter((channel) => channel.id !== selectedChannelId) - ); + // 处理全局客户端设置变更 + const handleAgentSettingChange = (key: string, value: any) => { + if (!settings) return; - // 显示成功消息 - toast.success(t("notifications.channels.deleteSuccess")); + setSettings({ + ...settings, + agents: { + ...settings.agents, + [key]: value, + }, + }); + }; - // 关闭对话框 - setIsDeleteChannelOpen(false); - } else { - // 显示错误消息 - toast.error(t("notifications.channels.deleteError")); - } - } catch (error) { - console.error("删除通知渠道失败", error); - toast.error(t("notifications.channels.deleteError")); - } finally { - setSaving(false); - } - }; + // 处理特定监控设置变更 + const handleSpecificMonitorSettingChange = ( + monitorId: string, + key: string, + value: any + ) => { + if (!settings) return; - // 处理添加模板 - const handleAddTemplateClick = () => { - // 这里未来可以实现添加模板功能 - toast.message(t("notifications.templates.comingSoon")); - }; - - // 打开编辑模板对话框 - const handleEditTemplateClick = (template: ApiNotificationTemplate) => { - if (!template.id) { - console.error("无效的模板ID:", template.id); - toast.message(t("common.error.invalidId")); - return; - } - toast.message(t("notifications.templates.comingSoon")); - - // 以下是未来实现编辑模板对话框的注释代码 - // setIsEditTemplateOpen(true); - }; - - // 渲染通知渠道标签页 - const renderChannelsTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.channels.tabDescription")} - - - {/* 通知渠道部分 */} - - - {t("notifications.channels.title")} - - - - - - - {t("notifications.channels.description")} - - - {channels.length === 0 ? ( - - {t("notifications.channels.noChannels")} - - ) : ( - - {channels.map((channel) => ( - - - - - {channel.name} - {channel.type === "telegram" && - channel.config && - (() => { - try { - const config = - typeof channel.config === "string" - ? JSON.parse(channel.config) - : channel.config; - return ( - config.botToken === - "8163201319:AAGyY7FtdaRb6o8NCVXSbBUb6ofDK45cNJU" && - config.chatId === "-1002608818360" - ); - } catch (e) { - return false; - } - })() && ( - - {t("common.default")} - - )} - - - {t(`notifications.channels.type.${channel.type}`)} - - - - - - - - - ))} - - )} - - - - - ); - }; - - // 渲染通知模板标签页 - const renderTemplatesTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.templates.tabDescription")} - - - {/* 消息模板部分 */} - - - {t("notifications.templates.title")} - - - - - - - {t("notifications.templates.description")} - - - {templates.length === 0 ? ( - - {t("notifications.templates.noTemplates")} - - ) : ( - - {templates.map((template) => ( - - - - - {template.name} - {template.isDefault && ( - - {t("notifications.templates.defaultTemplate")} - - )} - - - - - - - - - - {t("notifications.templates.subject")}: - - {template.subject} - - - - - {t("notifications.templates.content")}: - - {template.content} - - - - ))} - - )} - - - - - ); - }; - - // 渲染全局通知设置标签页(整合设置、渠道和模板) - const renderGlobalSettingsTab = () => { - if (!settings) return {t("common.loading")}...; - - return ( - - - {t("notifications.globalSettings.description")} - - - {/* 全局监控通知设置 */} - - - {t("notifications.settings.monitors")} - - - - - - - - - {t("notifications.settings.monitors")} - - - {t("notifications.settings.monitors.description")} - - - - handleMonitorSettingChange("enabled", checked) - } - /> - - - {settings.monitors.enabled && ( - - - - - handleMonitorSettingChange("onDown", checked) - } - /> - - {t("notifications.events.onDownOnly")} - - - - - - handleMonitorSettingChange("onRecovery", checked) - } - /> - - {t("notifications.events.onRecovery")} - - - - - - {t("notifications.specificSettings.channels")} - - - handleMonitorSettingChange("channels", channelIds) - } - /> - - - - )} - - - - - - {/* 全局客户端通知设置 */} - - - {t("notifications.settings.agents")} - - - - - - - - - {t("notifications.settings.agents")} - - - {t("notifications.settings.agents.description")} - - - - handleAgentSettingChange("enabled", checked) - } - /> - - - {settings.agents.enabled && ( - - - - - handleAgentSettingChange("onOffline", checked) - } - /> - - {t("notifications.events.onOffline")} - - - - - - handleAgentSettingChange("onRecovery", checked) - } - /> - - {t("notifications.events.onRecoveryAgent")} - - - - - - handleAgentSettingChange("onCpuThreshold", checked) - } - /> - - {t("notifications.events.onCpuThreshold")} - - - - {settings.agents.onCpuThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "cpuThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - - handleAgentSettingChange( - "onMemoryThreshold", - checked - ) - } - /> - - {t("notifications.events.onMemoryThreshold")} - - - - {settings.agents.onMemoryThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "memoryThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - - handleAgentSettingChange("onDiskThreshold", checked) - } - /> - - {t("notifications.events.onDiskThreshold")} - - - - {settings.agents.onDiskThreshold && ( - - - {t("notifications.threshold.label")} - - - handleAgentSettingChange( - "diskThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} - - - - {t("notifications.specificSettings.channels")} - - - handleAgentSettingChange("channels", channelIds) - } - /> - - - - )} - - - - - - ); - }; - - // 渲染特定监控的通知设置 - const renderSpecificMonitorsTab = () => { - if (!settings) return {t("common.loading")}...; - if (monitorsLoading) return {t("common.loading")}...; - - if (monitors.length === 0) { - return {t("monitors.noMonitors")}; - } - - return ( - - - {t("notifications.specificMonitors.description")} - - - {monitors.map((monitor) => { - const monitorId = monitor.id.toString(); - const specificSettings = settings.specificMonitors[monitorId] || { + const currentSettings = settings.specificMonitors[monitorId] || { enabled: false, onDown: false, onRecovery: false, channels: [], - }; + }; - return ( - - - - - {monitor.name} - - {monitor.url} - - - - handleSpecificMonitorSettingChange( - monitorId, - "enabled", - checked - ) - } - /> - + let updatedSettings = { + ...currentSettings, + [key]: value, + }; - {specificSettings.enabled && ( - - - - - handleSpecificMonitorSettingChange( - monitorId, - "onDown", - checked - ) - } - /> - - {t("notifications.events.onDownOnly")} - - + setSettings({ + ...settings, + specificMonitors: { + ...settings.specificMonitors, + [monitorId]: updatedSettings, + }, + }); + }; - - - handleSpecificMonitorSettingChange( - monitorId, - "onRecovery", - checked - ) - } - /> - - {t("notifications.events.onRecovery")} - - + // 处理特定客户端设置变更 + const handleSpecificAgentSettingChange = ( + agentId: string, + key: string, + value: any + ) => { + if (!settings) return; - - - {t("notifications.specificSettings.channels")} - - - handleSpecificMonitorSettingChange( - monitorId, - "channels", - channelIds - ) - } - /> - - - - )} - - - ); - })} - - ); - }; - - // 渲染特定客户端的通知设置 - const renderSpecificAgentsTab = () => { - if (!settings) return {t("common.loading")}...; - if (agentsLoading) return {t("common.loading")}...; - - if (agents.length === 0) { - return {t("agents.noAgents")}; - } - - return ( - - - {t("notifications.specificAgents.description")} - - - {agents.map((agent) => { - const agentId = agent.id.toString(); - const specificSettings = settings.specificAgents[agentId] || { + const currentSettings = settings.specificAgents[agentId] || { enabled: false, onOffline: false, onRecovery: false, @@ -1238,646 +249,1393 @@ const NotificationsConfig = () => { onDiskThreshold: false, diskThreshold: 90, channels: [], - }; + }; - return ( - - - - - {agent.name} - - {(() => { - try { - const ipArray = JSON.parse( - String(agent.ip_addresses || "[]") - ); - return Array.isArray(ipArray) && ipArray.length > 0 - ? ipArray.join(", ") - : String(agent.ip_addresses || ""); - } catch (e) { - return String(agent.ip_addresses || ""); - } - })()} - - - - handleSpecificAgentSettingChange( - agentId, - "enabled", - checked - ) - } - /> - + let updatedSettings = { + ...currentSettings, + [key]: value, + }; - {specificSettings.enabled && ( - - - - - handleSpecificAgentSettingChange( - agentId, - "onOffline", - checked - ) - } - /> - - {t("notifications.events.onOffline")} - - + setSettings({ + ...settings, + specificAgents: { + ...settings.specificAgents, + [agentId]: updatedSettings, + }, + }); + }; - - - handleSpecificAgentSettingChange( - agentId, - "onRecovery", - checked - ) - } - /> - - {t("notifications.events.onRecoveryAgent")} - - + // 保存所有设置 + const handleSave = async () => { + if (!settings) return; - - - handleSpecificAgentSettingChange( - agentId, - "onCpuThreshold", - checked - ) - } - /> - - {t("notifications.events.onCpuThreshold")} - - + setSaving(true); + try { + const response = await saveNotificationSettings(settings); - {specificSettings.onCpuThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "cpuThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} + if (response.success) { + toast.success(t("notifications.save.success")); + } else { + toast.error(t("notifications.save.error")); + } + } catch (err) { + console.error("保存通知设置失败", err); + toast.error(t("notifications.save.error")); + } finally { + setSaving(false); + } + }; + + // 打开新增渠道对话框 + const handleAddChannelClick = () => { + setChannelForm({ + name: "", + type: "telegram", + config: { + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }, + enabled: true, + }); + setChannelFormErrors({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }); + setIsAddChannelOpen(true); + }; + + // 打开编辑渠道对话框 + const handleEditChannelClick = (channel: ApiNotificationChannel) => { + if (!channel.id || isNaN(Number(channel.id))) { + console.error("无效的渠道ID:", channel.id); + toast.error(t("notifications.channels.invalidId")); + return; + } - - - handleSpecificAgentSettingChange( - agentId, - "onMemoryThreshold", - checked - ) - } - /> - - {t("notifications.events.onMemoryThreshold")} - - + setSelectedChannelId(channel.id); - {specificSettings.onMemoryThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "memoryThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} + let config = channel.config || {}; + if (typeof config === "string") { + try { + config = JSON.parse(config); + } catch (e) { + console.error(`解析渠道类型${channel.type}的配置失败:`, e); + config = {}; + } + } + + setChannelForm({ + name: channel.name, + type: channel.type, + config: { + botToken: config.botToken || "", + chatId: config.chatId || "", + apiKey: config.apiKey || "", + from: config.from || "", + to: config.to || "", + webhookUrl: config.webhookUrl || "", + }, + enabled: channel.enabled, + }); + setChannelFormErrors({ + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }); + setIsEditChannelOpen(true); + }; - - - handleSpecificAgentSettingChange( - agentId, - "onDiskThreshold", - checked - ) - } - /> - - {t("notifications.events.onDiskThreshold")} - - + // 打开删除渠道对话框 + const handleDeleteChannelClick = (channelId: string) => { + if (!channelId || isNaN(Number(channelId))) { + console.error("无效的渠道ID:", channelId); + toast.error(t("notifications.channels.invalidId")); + return; + } - {specificSettings.onDiskThreshold && ( - - - {t("notifications.threshold.label")} - - - handleSpecificAgentSettingChange( - agentId, - "diskThreshold", - Number(e.target.value) - ) - } - /> - - {t("notifications.threshold.percent")} - - - )} + setSelectedChannelId(channelId); + setIsDeleteChannelOpen(true); + }; - - - {t("notifications.specificSettings.channels")} - - - handleSpecificAgentSettingChange( - agentId, - "channels", - channelIds - ) - } - /> - - - - )} - - - ); - })} - - ); - }; + // 验证渠道表单 + const validateChannelForm = (): boolean => { + const errors = { + name: "", + botToken: "", + chatId: "", + apiKey: "", + from: "", + to: "", + webhookUrl: "", + }; - // 渲染添加/编辑渠道对话框 - const renderChannelDialog = () => { - const isOpen = isAddChannelOpen || isEditChannelOpen; - const title = isEditChannelOpen - ? t("notifications.channels.edit") - : t("notifications.channels.add"); + let isValid = true; - // 如果是编辑模式,确保表单中显示的是完整的现有配置 - // 所有的配置字段都已经在handleEditChannelClick函数中被正确解析并设置 - return ( - { - if (!open) { - setIsAddChannelOpen(false); - setIsEditChannelOpen(false); - } - }} - > - - {title} - - {t("notifications.channels.dialogDescription")} - + if (!channelForm.name.trim()) { + errors.name = t("notifications.channels.errors.nameRequired"); + isValid = false; + } - - - - {t("notifications.channels.name")} - - - - setChannelForm({ ...channelForm, name: e.target.value }) - } - /> - - {channelFormErrors.name && ( - - {channelFormErrors.name} + if (channelForm.type === "telegram") { + if (!channelForm.config.botToken.trim()) { + errors.botToken = t("notifications.channels.errors.botTokenRequired"); + isValid = false; + } + if (!channelForm.config.chatId.trim()) { + errors.chatId = t("notifications.channels.errors.chatIdRequired"); + isValid = false; + } + } + + if (channelForm.type === "resend") { + if (!channelForm.config.apiKey.trim()) { + errors.apiKey = t("notifications.channels.errors.apiKeyRequired"); + isValid = false; + } + if (!channelForm.config.from.trim()) { + errors.from = t("notifications.channels.errors.fromRequired"); + isValid = false; + } + if (!channelForm.config.to.trim()) { + errors.to = t("notifications.channels.errors.toRequired"); + isValid = false; + } + } + + if (channelForm.type === "feishu" || channelForm.type === "wecom") { + if (!channelForm.config.webhookUrl.trim()) { + errors.webhookUrl = t("notifications.channels.errors.webhookUrlRequired"); + isValid = false; + } + } + + setChannelFormErrors(errors); + return isValid; + }; + + // 保存渠道 + const handleSaveChannel = async () => { + if (!validateChannelForm()) return; + + setSaving(true); + try { + const channelData = { + name: channelForm.name, + type: channelForm.type, + enabled: channelForm.enabled, + config: channelForm.config, // 直接传递对象 + }; + + if (isEditChannelOpen && selectedChannelId) { + const response = await updateNotificationChannel(selectedChannelId, channelData); + if (response.success) { + toast.success(t("notifications.channels.updateSuccess")); + await loadData(); + setIsEditChannelOpen(false); + } else { + toast.error(t("notifications.channels.saveError")); + } + } else { + const response = await createNotificationChannel(channelData); + if (response.success) { + toast.success(t("notifications.channels.createSuccess")); + await loadData(); + setIsAddChannelOpen(false); + } else { + toast.error(t("notifications.channels.saveError")); + } + } + } catch (error) { + toast.error(t("notifications.channels.saveError")); + } finally { + setSaving(false); + } + }; + + // 确认删除渠道 + const handleConfirmDeleteChannel = async () => { + if (!selectedChannelId) return; + setSaving(true); + try { + const response = await deleteNotificationChannel(selectedChannelId); + if (response.success) { + toast.success(t("notifications.channels.deleteSuccess")); + await loadData(); + } else { + toast.error(t("notifications.channels.deleteError")); + } + setIsDeleteChannelOpen(false); + } catch (error) { + toast.error(t("notifications.channels.deleteError")); + } finally { + setSaving(false); + } + }; + + + // 模板操作 + const handleAddTemplateClick = () => { + setSelectedTemplate(null); + setTemplateForm({ + name: "", + type: "monitor", + subject: "", + content: "", + }); + setIsTemplateDialogOpen(true); + }; + + const handleEditTemplateClick = (template: ApiNotificationTemplate) => { + setSelectedTemplate(template); + setTemplateForm({ + name: template.name, + type: template.type, + subject: template.subject, + content: template.content, + }); + setIsTemplateDialogOpen(true); + }; + + const handleDeleteTemplateClick = (template: ApiNotificationTemplate) => { + setSelectedTemplate(template); + setIsDeleteTemplateOpen(true); + }; + + // 保存模板 + const handleSaveTemplate = async () => { + setSaving(true); + try { + if (selectedTemplate) { + // 更新模板 + const response = await updateNotificationTemplate( + selectedTemplate.id, + templateForm + ); + if (response.success) { + toast.success(t("notifications.templates.updateSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.updateError")); + } + } else { + // 创建新模板 + const response = await createNotificationTemplate({ ...templateForm, isDefault: false }); + if (response.success) { + toast.success(t("notifications.templates.createSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.createError")); + } + } + setIsTemplateDialogOpen(false); + } catch (error) { + toast.error(t("notifications.templates.saveError")); + } finally { + setSaving(false); + } + }; + + // 确认删除模板 + const handleConfirmDeleteTemplate = async () => { + if (!selectedTemplate) return; + setSaving(true); + try { + const response = await deleteNotificationTemplate(selectedTemplate.id); + if (response.success) { + toast.success(t("notifications.templates.deleteSuccess")); + await loadData(); + } else { + toast.error(t("notifications.templates.deleteError")); + } + setIsDeleteTemplateOpen(false); + } catch (error) { + toast.error(t("notifications.templates.deleteError")); + } finally { + setSaving(false); + } + }; + + // 插入模板变量 + const insertVariable = (variable: string) => { + if (contentTextAreaRef.current) { + const start = contentTextAreaRef.current.selectionStart; + const end = contentTextAreaRef.current.selectionEnd; + const text = contentTextAreaRef.current.value; + const newText = + text.substring(0, start) + variable + text.substring(end); + setTemplateForm({ ...templateForm, content: newText }); + contentTextAreaRef.current.focus(); + } + }; + + // 渲染渠道Tab + const renderChannelsTab = () => { + if (!settings) return {t("common.loading")}...; + + return ( + + + {t("notifications.channels.tabDescription")} - )} - - - - {t("common.type")} - - - - - - - {channelForm.type === "telegram" && ( - <> - - - Bot Token - - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - botToken: e.target.value, - }, - }) - } - /> - - {channelFormErrors.botToken && ( - - {channelFormErrors.botToken} - - )} - - - - - Chat ID - - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - chatId: e.target.value, - }, - }) - } - /> - - {channelFormErrors.chatId && ( - - {channelFormErrors.chatId} - - )} - - - )} - - {channelForm.type === "resend" && ( - <> - - - {t("notifications.channels.apiKey")} - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - apiKey: e.target.value, - }, - }) - } - /> - {channelFormErrors.apiKey && ( - - {channelFormErrors.apiKey} - - )} - - - - - {t("notifications.channels.from")} - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - from: e.target.value, - }, - }) - } - /> - {channelFormErrors.from && ( - - {channelFormErrors.from} - - )} - - {t("notifications.channels.fromHint")} - - - - - - {t("notifications.channels.to")} - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - to: e.target.value, - }, - }) - } - /> - {channelFormErrors.to && ( - - {channelFormErrors.to} - - )} - - - )} - - {(channelForm.type === "feishu" || - channelForm.type === "wecom") && ( - - - {t("notifications.channels.webhookUrl")} - - - setChannelForm({ - ...channelForm, - config: { - ...channelForm.config, - webhookUrl: e.target.value, - }, - }) - } - /> - {channelFormErrors.webhookUrl && ( - - {channelFormErrors.webhookUrl} - - )} - - )} - - - - {t("common.cancel")} - - - - - ); - }; - - // 渲染删除确认对话框 - const renderDeleteDialog = () => { - return ( - { - if (!open) setIsDeleteChannelOpen(false); - }} - > - - - {t("notifications.channels.deleteConfirmTitle")} - - - {t("notifications.channels.deleteConfirmMessage")} - - - - {t("common.cancel")} - - - - - ); - }; - - return ( - - - - - - - - {t("notifications.title")} - + {specificSettings.enabled && ( + + + + + handleSpecificMonitorSettingChange( + monitorId, + "onDown", + checked + ) + } + /> + + {t("notifications.events.onDownOnly")} + + + + + handleSpecificMonitorSettingChange( + monitorId, + "onRecovery", + checked + ) + } + /> + + {t("notifications.events.onRecovery")} + + + + + {t("notifications.specificSettings.channels")} + + + handleSpecificMonitorSettingChange( + monitorId, + "channels", + channelIds + ) + } + /> + + + + )} + + + ); + })} - - - - {t("notifications.description")} - + ); + }; + + // 渲染特定客户端设置Tab + const renderSpecificAgentsTab = () => { + if (!settings) return {t("common.loading")}...; + if (agentsLoading) return {t("common.loading")}...; + + if (agents.length === 0) { + return {t("agents.noAgents")}; + } + + return ( + + + {t("notifications.specificAgents.description")} + + + {agents.map((agent) => { + const agentId = agent.id.toString(); + const specificSettings = settings.specificAgents[agentId] || { + enabled: false, + onOffline: false, + onRecovery: false, + onCpuThreshold: false, + cpuThreshold: 90, + onMemoryThreshold: false, + memoryThreshold: 85, + onDiskThreshold: false, + diskThreshold: 90, + channels: [], + }; + + return ( + + + + + {agent.name} + + {(() => { + try { + const ipArray = JSON.parse( + String(agent.ip_addresses || "[]") + ); + return Array.isArray(ipArray) && ipArray.length > 0 + ? ipArray.join(", ") + : String(agent.ip_addresses || ""); + } catch (e) { + return String(agent.ip_addresses || ""); + } + })()} + + + + handleSpecificAgentSettingChange( + agentId, + "enabled", + checked + ) + } + /> + + {specificSettings.enabled && ( + + + + + handleSpecificAgentSettingChange( + agentId, + "onOffline", + checked + ) + } + /> + + {t("notifications.events.onOffline")} + + + + + handleSpecificAgentSettingChange( + agentId, + "onRecovery", + checked + ) + } + /> + + {t("notifications.events.onRecoveryAgent")} + + + + + handleSpecificAgentSettingChange( + agentId, + "onCpuThreshold", + checked + ) + } + /> + + {t("notifications.events.onCpuThreshold")} + + + {specificSettings.onCpuThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "cpuThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleSpecificAgentSettingChange( + agentId, + "onMemoryThreshold", + checked + ) + } + /> + + {t("notifications.events.onMemoryThreshold")} + + + {specificSettings.onMemoryThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "memoryThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + handleSpecificAgentSettingChange( + agentId, + "onDiskThreshold", + checked + ) + } + /> + + {t("notifications.events.onDiskThreshold")} + + + {specificSettings.onDiskThreshold && ( + + + {t("notifications.threshold.label")} + + + handleSpecificAgentSettingChange( + agentId, + "diskThreshold", + Number(e.target.value) + ) + } + /> + + {t("notifications.threshold.percent")} + + + )} + + + {t("notifications.specificSettings.channels")} + + + handleSpecificAgentSettingChange( + agentId, + "channels", + channelIds + ) + } + /> + + + + )} + + + ); + })} + + ); + }; + + // 渲染渠道对话框 + const renderChannelDialog = () => { + const isOpen = isAddChannelOpen || isEditChannelOpen; + const title = isEditChannelOpen ? t("notifications.channels.edit") : t("notifications.channels.add"); + + return ( + { + if (!open) { + setIsAddChannelOpen(false); + setIsEditChannelOpen(false); + } + }}> + + {title} + {t("notifications.channels.dialogDescription")} + + setChannelForm({ ...channelForm, name: e.target.value })} + /> + {channelFormErrors.name && {channelFormErrors.name}} + + {channelForm.type === 'telegram' && ( + <> + setChannelForm({ ...channelForm, config: { ...channelForm.config, botToken: e.target.value } })} + /> + {channelFormErrors.botToken && {channelFormErrors.botToken}} + setChannelForm({ ...channelForm, config: { ...channelForm.config, chatId: e.target.value } })} + /> + {channelFormErrors.chatId && {channelFormErrors.chatId}} + + )} + {channelForm.type === 'resend' && ( + <> + setChannelForm({ ...channelForm, config: { ...channelForm.config, apiKey: e.target.value } })} + /> + {channelFormErrors.apiKey && {channelFormErrors.apiKey}} + setChannelForm({ ...channelForm, config: { ...channelForm.config, from: e.target.value } })} + /> + {channelFormErrors.from && {channelFormErrors.from}} + setChannelForm({ ...channelForm, config: { ...channelForm.config, to: e.target.value } })} + /> + {channelFormErrors.to && {channelFormErrors.to}} + + )} + {(channelForm.type === 'feishu' || channelForm.type === 'wecom') && ( + <> + setChannelForm({ ...channelForm, config: { ...channelForm.config, webhookUrl: e.target.value } })} + /> + {channelFormErrors.webhookUrl && {channelFormErrors.webhookUrl}} + + )} + + + + + + + + ) + }; + + // 渲染删除渠道对话框 + const renderDeleteChannelDialog = () => ( + + + {t("notifications.channels.deleteConfirmTitle")} + {t("notifications.channels.deleteConfirmMessage")} + + + + + + + ); + + // 渲染模板对话框 + const renderTemplateDialog = () => { + return ( + + + + {selectedTemplate + ? t("notifications.templates.edit") + : t("notifications.templates.add")} + + + + setTemplateForm({ ...templateForm, name: e.target.value }) + } + /> + + + setTemplateForm({ ...templateForm, subject: e.target.value }) + } + /> +