fix: get notification template error

This commit is contained in:
zaunist committed 2025-11-06 00:49:09 +08:00
1 parent a6a9976852
commit af7a006c16
16 files changed
+6226 -1914

No files matched your search

File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -33,9 +33,9 @@
{
"idx": 4,
"version": "6",
"when": 1751016668000,
"tag": "0004_lame_mercenary",
"when": 1762361284317,
"tag": "0004_many_avengers",
"breakpoints": true
}
]
}
}
+1 -1
View File
@@ -228,7 +228,7 @@ CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_monitor_timestamp_idx\`
sql: `CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_timestamp_idx\` ON \`monitor_status_history_24h\` (\`timestamp\`);`
},
{
name: "0004_lame_mercenary.sql",
name: "0004_many_avengers.sql",
sql: `CREATE TABLE IF NOT EXISTS \`settings\` (
\`key\` text PRIMARY KEY NOT NULL,
\`value\` text
+144 -77
View File
@@ -1,4 +1,11 @@
import { int, sqliteTable, text, real, primaryKey, index } from "drizzle-orm/sqlite-core";
import {
int,
sqliteTable,
text,
real,
primaryKey,
index,
} from "drizzle-orm/sqlite-core";
// 用户表
export const users = sqliteTable("users", {
@@ -8,7 +15,7 @@ export const users = sqliteTable("users", {
email: text("email"),
role: text("role").notNull(),
created_at: text("created_at").notNull(),
updated_at: text("updated_at").notNull()
updated_at: text("updated_at").notNull(),
});
// 监控表
@@ -22,54 +29,77 @@ export const monitors = sqliteTable("monitors", {
expected_status: int("expected_status").notNull(),
headers: text("headers").notNull(),
body: text("body"),
created_by: int("created_by").notNull().references(() => users.id),
created_by: int("created_by")
.notNull()
.references(() => users.id),
active: int("active").notNull(), // SQLite 没有布尔类型,用 int 代替
status: text("status").default("pending"),
response_time: int("response_time").default(0),
last_checked: text("last_checked"),
created_at: text("created_at").notNull(),
updated_at: text("updated_at").notNull()
updated_at: text("updated_at").notNull(),
});
// 24小时监控状态历史表
export const monitorStatusHistory24h = sqliteTable("monitor_status_history_24h", {
id: int("id").primaryKey({ autoIncrement: true }),
monitor_id: int("monitor_id").notNull().references(() => monitors.id),
status: text("status").notNull(),
timestamp: text("timestamp").default("CURRENT_TIMESTAMP"),
response_time: int("response_time"),
status_code: int("status_code"),
error: text("error")
}, (table) => ({
// monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能
monitorTimestampIdx: index("monitor_status_history_24h_monitor_timestamp_idx").on(table.monitor_id, table.timestamp),
// timestamp 单独索引,用于优化按时间排序和范围查询的性能
timestampIdx: index("monitor_status_history_24h_timestamp_idx").on(table.timestamp)
}));
export const monitorStatusHistory24h = sqliteTable(
"monitor_status_history_24h",
{
id: int("id").primaryKey({ autoIncrement: true }),
monitor_id: int("monitor_id")
.notNull()
.references(() => monitors.id),
status: text("status").notNull(),
timestamp: text("timestamp").default("CURRENT_TIMESTAMP"),
response_time: int("response_time"),
status_code: int("status_code"),
error: text("error"),
},
(table) => ({
// monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能
monitorTimestampIdx: index(
"monitor_status_history_24h_monitor_timestamp_idx"
).on(table.monitor_id, table.timestamp),
// timestamp 单独索引,用于优化按时间排序和范围查询的性能
timestampIdx: index("monitor_status_history_24h_timestamp_idx").on(
table.timestamp
),
})
);
// 监控每日统计表
export const monitorDailyStats = sqliteTable("monitor_daily_stats", {
id: int("id").primaryKey({ autoIncrement: true }),
monitor_id: int("monitor_id").notNull().references(() => monitors.id),
date: text("date").notNull(),
total_checks: int("total_checks").notNull().default(0),
up_checks: int("up_checks").notNull().default(0),
down_checks: int("down_checks").notNull().default(0),
avg_response_time: int("avg_response_time").default(0),
min_response_time: int("min_response_time").default(0),
max_response_time: int("max_response_time").default(0),
availability: real("availability").default(0),
created_at: text("created_at").notNull()
}, (table) => ({
monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on(table.monitor_id, table.date)
}));
export const monitorDailyStats = sqliteTable(
"monitor_daily_stats",
{
id: int("id").primaryKey({ autoIncrement: true }),
monitor_id: int("monitor_id")
.notNull()
.references(() => monitors.id),
date: text("date").notNull(),
total_checks: int("total_checks").notNull().default(0),
up_checks: int("up_checks").notNull().default(0),
down_checks: int("down_checks").notNull().default(0),
avg_response_time: int("avg_response_time").default(0),
min_response_time: int("min_response_time").default(0),
max_response_time: int("max_response_time").default(0),
availability: real("availability").default(0),
created_at: text("created_at").notNull(),
},
(table) => ({
monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on(
table.monitor_id,
table.date
),
})
);
// 客户端表
export const agents = sqliteTable("agents", {
id: int("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
token: text("token").notNull().unique(),
created_by: int("created_by").notNull().references(() => users.id),
created_by: int("created_by")
.notNull()
.references(() => users.id),
status: text("status").default("inactive"),
created_at: text("created_at").notNull(),
updated_at: text("updated_at").notNull(),
@@ -81,54 +111,81 @@ export const agents = sqliteTable("agents", {
});
// 客户端资源指标表
export const agentMetrics24h = sqliteTable("agent_metrics_24h", {
id: int("id").primaryKey({ autoIncrement: true }),
agent_id: int("agent_id").notNull().references(() => agents.id),
timestamp: text("timestamp").default("CURRENT_TIMESTAMP"),
cpu_usage: real("cpu_usage"),
cpu_cores: int("cpu_cores"),
cpu_model: text("cpu_model"),
memory_total: int("memory_total"),
memory_used: int("memory_used"),
memory_free: int("memory_free"),
memory_usage_rate: real("memory_usage_rate"),
load_1: real("load_1"),
load_5: real("load_5"),
load_15: real("load_15"),
disk_metrics: text("disk_metrics"),
network_metrics: text("network_metrics")
}, (table) => ({
// agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能
agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on(table.agent_id, table.timestamp)
}));
export const agentMetrics24h = sqliteTable(
"agent_metrics_24h",
{
id: int("id").primaryKey({ autoIncrement: true }),
agent_id: int("agent_id")
.notNull()
.references(() => agents.id),
timestamp: text("timestamp").default("CURRENT_TIMESTAMP"),
cpu_usage: real("cpu_usage"),
cpu_cores: int("cpu_cores"),
cpu_model: text("cpu_model"),
memory_total: int("memory_total"),
memory_used: int("memory_used"),
memory_free: int("memory_free"),
memory_usage_rate: real("memory_usage_rate"),
load_1: real("load_1"),
load_5: real("load_5"),
load_15: real("load_15"),
disk_metrics: text("disk_metrics"),
network_metrics: text("network_metrics"),
},
(table) => ({
// agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能
agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on(
table.agent_id,
table.timestamp
),
})
);
// 状态页配置表
export const statusPageConfig = sqliteTable("status_page_config", {
id: int("id").primaryKey({ autoIncrement: true }),
user_id: int("user_id").notNull().references(() => users.id),
user_id: int("user_id")
.notNull()
.references(() => users.id),
title: text("title").notNull().default("系统状态"),
description: text("description").default("系统当前运行状态"),
logo_url: text("logo_url").default(""),
custom_css: text("custom_css").default(""),
created_at: text("created_at").default("CURRENT_TIMESTAMP"),
updated_at: text("updated_at").default("CURRENT_TIMESTAMP")
updated_at: text("updated_at").default("CURRENT_TIMESTAMP"),
});
// 状态页监控项关联表
export const statusPageMonitors = sqliteTable("status_page_monitors", {
config_id: int("config_id").notNull().references(() => statusPageConfig.id, { onDelete: "cascade" }),
monitor_id: int("monitor_id").notNull().references(() => monitors.id, { onDelete: "cascade" }),
}, (table) => ({
pk: primaryKey({ columns: [table.config_id, table.monitor_id] })
}));
export const statusPageMonitors = sqliteTable(
"status_page_monitors",
{
config_id: int("config_id")
.notNull()
.references(() => statusPageConfig.id, { onDelete: "cascade" }),
monitor_id: int("monitor_id")
.notNull()
.references(() => monitors.id, { onDelete: "cascade" }),
},
(table) => ({
pk: primaryKey({ columns: [table.config_id, table.monitor_id] }),
})
);
// 状态页客户端关联表
export const statusPageAgents = sqliteTable("status_page_agents", {
config_id: int("config_id").notNull().references(() => statusPageConfig.id, { onDelete: "cascade" }),
agent_id: int("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
}, (table) => ({
pk: primaryKey({ columns: [table.config_id, table.agent_id] })
}));
export const statusPageAgents = sqliteTable(
"status_page_agents",
{
config_id: int("config_id")
.notNull()
.references(() => statusPageConfig.id, { onDelete: "cascade" }),
agent_id: int("agent_id")
.notNull()
.references(() => agents.id, { onDelete: "cascade" }),
},
(table) => ({
pk: primaryKey({ columns: [table.config_id, table.agent_id] }),
})
);
// 通知渠道表
export const notificationChannels = sqliteTable("notification_channels", {
@@ -137,9 +194,11 @@ export const notificationChannels = sqliteTable("notification_channels", {
type: text("type").notNull(),
config: text("config").notNull(),
enabled: int("enabled").notNull().default(1),
created_by: int("created_by").notNull().references(() => users.id),
created_by: int("created_by")
.notNull()
.references(() => users.id),
created_at: text("created_at").default("CURRENT_TIMESTAMP"),
updated_at: text("updated_at").default("CURRENT_TIMESTAMP")
updated_at: text("updated_at").default("CURRENT_TIMESTAMP"),
});
// 通知模板表
@@ -150,15 +209,19 @@ export const notificationTemplates = sqliteTable("notification_templates", {
subject: text("subject").notNull(),
content: text("content").notNull(),
is_default: int("is_default").notNull().default(0),
created_by: int("created_by").notNull().references(() => users.id),
created_by: int("created_by")
.notNull()
.references(() => users.id),
created_at: text("created_at").default("CURRENT_TIMESTAMP"),
updated_at: text("updated_at").default("CURRENT_TIMESTAMP")
updated_at: text("updated_at").default("CURRENT_TIMESTAMP"),
});
// 通知设置表
export const notificationSettings = sqliteTable("notification_settings", {
id: int("id").primaryKey({ autoIncrement: true }),
user_id: int("user_id").notNull().references(() => users.id),
user_id: int("user_id")
.notNull()
.references(() => users.id),
target_type: text("target_type").notNull().default("global"),
target_id: int("target_id"),
enabled: int("enabled").notNull().default(1),
@@ -173,7 +236,7 @@ export const notificationSettings = sqliteTable("notification_settings", {
disk_threshold: int("disk_threshold").notNull().default(90),
channels: text("channels").default("[]"),
created_at: text("created_at").default("CURRENT_TIMESTAMP"),
updated_at: text("updated_at").default("CURRENT_TIMESTAMP")
updated_at: text("updated_at").default("CURRENT_TIMESTAMP"),
});
// 通知历史记录表
@@ -181,12 +244,16 @@ export const notificationHistory = sqliteTable("notification_history", {
id: int("id").primaryKey({ autoIncrement: true }),
type: text("type").notNull(),
target_id: int("target_id"),
channel_id: int("channel_id").notNull().references(() => notificationChannels.id),
template_id: int("template_id").notNull().references(() => notificationTemplates.id),
channel_id: int("channel_id")
.notNull()
.references(() => notificationChannels.id),
template_id: int("template_id")
.notNull()
.references(() => notificationTemplates.id),
status: text("status").notNull(),
content: text("content").notNull(),
error: text("error"),
sent_at: text("sent_at").default("CURRENT_TIMESTAMP")
sent_at: text("sent_at").default("CURRENT_TIMESTAMP"),
});
// 新增:应用设置表
+27 -7
View File
@@ -182,15 +182,35 @@ export const updateNotificationTemplate = async (
Omit<NotificationTemplate, "id" | "created_at" | "updated_at">
>
): Promise<boolean> => {
const updateData: Partial<typeof notificationTemplates.$inferInsert> = {};
if (template.name !== undefined) {
updateData.name = template.name;
}
if (template.type !== undefined) {
updateData.type = template.type;
}
if (template.subject !== undefined) {
updateData.subject = template.subject;
}
if (template.content !== undefined) {
updateData.content = template.content;
}
if (template.is_default !== undefined) {
updateData.is_default = template.is_default ? 1 : 0;
}
if (Object.keys(updateData).length === 0) {
return true; // 没有需要更新的字段时视为成功
}
const result = await db
.update(notificationTemplates)
.set({
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
is_default: template.is_default ? 1 : 0,
})
.set(updateData)
.where(and(eq(notificationTemplates.id, id), eq(notificationTemplates.created_by, userId)));
return result.success; // fix: 返回 success 布尔值
+13 -3
View File
@@ -677,13 +677,23 @@ export async function sendNotification(
const templates = await repositories.getNotificationTemplates(userId);
console.log(`[发送通知] 获取到${templates.length}个通知模板`);
const defaultTemplate = templates.find(
let defaultTemplate = templates.find(
(t) => t.is_default && t.type === type
);
if (!defaultTemplate) {
console.error(`[发送通知] 找不到类型为${type}的默认通知模板`);
return { success: false, results: [] };
const fallbackTemplate = templates.find((t) => t.type === type);
if (!fallbackTemplate) {
console.error(`[发送通知] 找不到类型为${type}的任何通知模板`);
return { success: false, results: [] };
}
console.warn(
`[发送通知] 找不到类型为${type}的默认通知模板,使用ID=${fallbackTemplate.id}作为回退模板`
);
defaultTemplate = fallbackTemplate;
}
console.log(
+341 -180
View File
@@ -1,187 +1,313 @@
import api from "./client";
import type {
NotificationChannel,
NotificationConfig,
NotificationTemplate,
} from "../types/notification";
// 通知渠道类型
export interface NotificationChannel {
id: string;
type BackendNotificationChannel = {
id: number;
name: string;
type: string;
config: Record<string, any>;
enabled: boolean;
}
config: string | Record<string, unknown> | null;
enabled: number | boolean;
created_by?: number;
created_at?: string;
updated_at?: string;
};
// 通知模板类型
export interface NotificationTemplate {
id: string;
type BackendNotificationTemplate = {
id: number;
name: string;
type: string;
subject: string;
content: string;
isDefault: boolean;
}
is_default: number | boolean;
created_by?: number;
created_at?: string;
updated_at?: string;
};
// 通知设置类型
export interface NotificationSettings {
monitors: {
enabled: boolean;
onDown: boolean;
onRecovery: boolean;
channels: string[];
};
agents: {
enabled: boolean;
onOffline: boolean;
onRecovery: boolean;
onCpuThreshold: boolean;
cpuThreshold: number;
onMemoryThreshold: boolean;
memoryThreshold: number;
onDiskThreshold: boolean;
diskThreshold: number;
channels: string[];
};
specificMonitors: {
[monitorId: string]: {
enabled: boolean;
onDown: boolean;
onRecovery: boolean;
channels: string[];
};
};
specificAgents: {
[agentId: string]: {
enabled: boolean;
onOffline: boolean;
onRecovery: boolean;
onCpuThreshold: boolean;
cpuThreshold: number;
onMemoryThreshold: boolean;
memoryThreshold: number;
onDiskThreshold: boolean;
diskThreshold: number;
channels: string[];
};
};
}
export type NotificationSettings = NotificationConfig["settings"];
// 通知配置响应类型
export interface NotificationConfigResponse {
success: boolean;
message?: string;
data?: {
channels: NotificationChannel[];
templates: NotificationTemplate[];
settings: NotificationSettings;
};
data?: NotificationConfig;
}
const parseChannelConfig = (
config: BackendNotificationChannel["config"]
): Record<string, unknown> => {
if (!config) {
return {};
}
if (typeof config === "string") {
try {
const parsed = JSON.parse(config);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
} catch (error) {
console.error("解析通知渠道配置失败:", error);
}
return {};
}
if (typeof config === "object" && !Array.isArray(config)) {
return config as Record<string, unknown>;
}
return {};
};
const normalizeBoolean = (value: unknown, fallback = false): boolean => {
if (typeof value === "boolean") {
return value;
}
if (typeof value === "number") {
return value !== 0;
}
if (typeof value === "string") {
return value === "1" || value.toLowerCase() === "true";
}
return fallback;
};
const normalizeNumber = (value: unknown, fallback: number): number => {
const num = Number(value);
return Number.isFinite(num) ? num : fallback;
};
const normalizeChannelIds = (value: unknown): number[] => {
if (!value) {
return [];
}
if (typeof value === "string") {
try {
const parsed = JSON.parse(value);
if (Array.isArray(parsed)) {
return parsed
.map((item) => Number(item))
.filter((item) => Number.isInteger(item));
}
} catch (error) {
console.error("解析通知渠道ID列表失败:", error);
return [];
}
}
if (Array.isArray(value)) {
return value
.map((item) => Number(item))
.filter((item) => Number.isInteger(item));
}
return [];
};
const transformChannel = (
channel: BackendNotificationChannel
): NotificationChannel => ({
id: Number(channel.id),
name: channel.name,
type: channel.type,
config: parseChannelConfig(channel.config),
enabled: normalizeBoolean(channel.enabled, true),
createdBy: channel.created_by,
createdAt: channel.created_at,
updatedAt: channel.updated_at,
});
const transformTemplate = (
template: BackendNotificationTemplate
): NotificationTemplate => ({
id: Number(template.id),
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
isDefault: normalizeBoolean(template.is_default, false),
createdBy: template.created_by,
createdAt: template.created_at,
updatedAt: template.updated_at,
});
const normalizeSettings = (settings: any): NotificationSettings => {
const normalized: NotificationSettings = {
monitors: {
enabled: false,
onDown: false,
onRecovery: false,
channels: [],
},
agents: {
enabled: false,
onOffline: false,
onRecovery: false,
onCpuThreshold: false,
cpuThreshold: 90,
onMemoryThreshold: false,
memoryThreshold: 85,
onDiskThreshold: false,
diskThreshold: 90,
channels: [],
},
specificMonitors: {},
specificAgents: {},
};
if (!settings) {
return normalized;
}
if (settings.monitors) {
normalized.monitors = {
enabled: normalizeBoolean(settings.monitors.enabled),
onDown: normalizeBoolean(settings.monitors.onDown ?? settings.monitors.on_down),
onRecovery: normalizeBoolean(
settings.monitors.onRecovery ?? settings.monitors.on_recovery
),
channels: normalizeChannelIds(settings.monitors.channels),
};
}
if (settings.agents) {
normalized.agents = {
enabled: normalizeBoolean(settings.agents.enabled),
onOffline: normalizeBoolean(settings.agents.onOffline ?? settings.agents.on_offline),
onRecovery: normalizeBoolean(
settings.agents.onRecovery ?? settings.agents.on_recovery
),
onCpuThreshold: normalizeBoolean(
settings.agents.onCpuThreshold ?? settings.agents.on_cpu_threshold
),
cpuThreshold: normalizeNumber(settings.agents.cpuThreshold ?? settings.agents.cpu_threshold, 90),
onMemoryThreshold: normalizeBoolean(
settings.agents.onMemoryThreshold ?? settings.agents.on_memory_threshold
),
memoryThreshold: normalizeNumber(
settings.agents.memoryThreshold ?? settings.agents.memory_threshold,
85
),
onDiskThreshold: normalizeBoolean(
settings.agents.onDiskThreshold ?? settings.agents.on_disk_threshold
),
diskThreshold: normalizeNumber(
settings.agents.diskThreshold ?? settings.agents.disk_threshold,
90
),
channels: normalizeChannelIds(settings.agents.channels),
};
}
if (settings.specificMonitors) {
Object.entries(settings.specificMonitors).forEach(
([monitorId, monitorSetting]: [string, any]) => {
normalized.specificMonitors[monitorId] = {
enabled: normalizeBoolean(monitorSetting?.enabled),
onDown: normalizeBoolean(
monitorSetting?.onDown ?? monitorSetting?.on_down
),
onRecovery: normalizeBoolean(
monitorSetting?.onRecovery ?? monitorSetting?.on_recovery
),
channels: normalizeChannelIds(monitorSetting?.channels),
};
}
);
}
if (settings.specificAgents) {
Object.entries(settings.specificAgents).forEach(
([agentId, agentSetting]: [string, any]) => {
normalized.specificAgents[agentId] = {
enabled: normalizeBoolean(agentSetting?.enabled),
onOffline: normalizeBoolean(
agentSetting?.onOffline ?? agentSetting?.on_offline
),
onRecovery: normalizeBoolean(
agentSetting?.onRecovery ?? agentSetting?.on_recovery
),
onCpuThreshold: normalizeBoolean(
agentSetting?.onCpuThreshold ?? agentSetting?.on_cpu_threshold
),
cpuThreshold: normalizeNumber(
agentSetting?.cpuThreshold ?? agentSetting?.cpu_threshold,
normalized.agents.cpuThreshold
),
onMemoryThreshold: normalizeBoolean(
agentSetting?.onMemoryThreshold ?? agentSetting?.on_memory_threshold
),
memoryThreshold: normalizeNumber(
agentSetting?.memoryThreshold ?? agentSetting?.memory_threshold,
normalized.agents.memoryThreshold
),
onDiskThreshold: normalizeBoolean(
agentSetting?.onDiskThreshold ?? agentSetting?.on_disk_threshold
),
diskThreshold: normalizeNumber(
agentSetting?.diskThreshold ?? agentSetting?.disk_threshold,
normalized.agents.diskThreshold
),
channels: normalizeChannelIds(agentSetting?.channels),
};
}
);
}
return normalized;
};
// 获取完整的通知配置
export const getNotificationConfig =
async (): Promise<NotificationConfigResponse> => {
try {
const response = await api.get<NotificationConfigResponse>(
"/api/notifications"
);
const response = await api.get<{
success: boolean;
message?: string;
data?: {
channels?: BackendNotificationChannel[];
templates?: BackendNotificationTemplate[];
settings?: any;
};
}>("/api/notifications");
// 如果响应成功并且有数据,处理数据格式
if (response.data.success && response.data.data) {
// 处理渠道配置 - 确保config字段是对象而不是字符串
if (response.data.data.channels) {
response.data.data.channels = response.data.data.channels.map(
(channel) => {
// 使用类型断言访问后端特有属性
const backendChannel = channel as any;
return {
id: channel.id,
name: channel.name,
type: channel.type,
// 将字符串类型的config解析为对象
config:
typeof backendChannel.config === "string"
? JSON.parse(backendChannel.config)
: backendChannel.config,
enabled: !!backendChannel.enabled,
};
}
);
}
const backendData = response.data.data;
// 处理模板配置 - 确保is_default是布尔类型
if (response.data.data.templates) {
response.data.data.templates = response.data.data.templates.map(
(template) => {
// 使用类型断言访问后端特有属性
const backendTemplate = template as any;
return {
id: template.id,
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
isDefault: !!backendTemplate.is_default,
};
}
);
}
// 确保settings中的所有布尔字段是真正的布尔类型而不是0/1
if (response.data.data.settings) {
const { settings } = response.data.data;
// 处理全局监控设置
if (settings.monitors) {
settings.monitors = {
...settings.monitors,
enabled: !!settings.monitors.enabled,
onDown: !!settings.monitors.onDown,
onRecovery: !!settings.monitors.onRecovery,
};
}
// 处理全局客户端设置
if (settings.agents) {
settings.agents = {
...settings.agents,
enabled: !!settings.agents.enabled,
onOffline: !!settings.agents.onOffline,
onRecovery: !!settings.agents.onRecovery,
onCpuThreshold: !!settings.agents.onCpuThreshold,
onMemoryThreshold: !!settings.agents.onMemoryThreshold,
onDiskThreshold: !!settings.agents.onDiskThreshold,
};
}
// 处理特定监控设置
if (settings.specificMonitors) {
Object.keys(settings.specificMonitors).forEach((monitorId) => {
const monitorSetting = settings.specificMonitors[monitorId];
settings.specificMonitors[monitorId] = {
...monitorSetting,
enabled: !!monitorSetting.enabled,
onDown: !!monitorSetting.onDown,
onRecovery: !!monitorSetting.onRecovery,
};
});
}
// 处理特定客户端设置
if (settings.specificAgents) {
Object.keys(settings.specificAgents).forEach((agentId) => {
const agentSetting = settings.specificAgents[agentId];
settings.specificAgents[agentId] = {
...agentSetting,
enabled: !!agentSetting.enabled,
onOffline: !!agentSetting.onOffline,
onRecovery: !!agentSetting.onRecovery,
onCpuThreshold: !!agentSetting.onCpuThreshold,
onMemoryThreshold: !!agentSetting.onMemoryThreshold,
onDiskThreshold: !!agentSetting.onDiskThreshold,
};
});
}
}
if (!backendData) {
return {
success: response.data.success,
message: response.data.message,
};
}
return response.data;
const channels = Array.isArray(backendData.channels)
? (backendData.channels as BackendNotificationChannel[]).map(
transformChannel
)
: [];
const templates = Array.isArray(backendData.templates)
? (backendData.templates as BackendNotificationTemplate[]).map(
transformTemplate
)
: [];
return {
success: response.data.success,
message: response.data.message,
data: {
channels,
templates,
settings: normalizeSettings(backendData.settings),
},
};
} catch (error) {
console.error("获取通知配置失败:", error);
return {
@@ -201,13 +327,17 @@ export const getNotificationChannels = async (): Promise<{
const response = await api.get<{
success: boolean;
message?: string;
data?: NotificationChannel[];
data?: BackendNotificationChannel[];
}>("/api/notifications/channels");
const channels = Array.isArray(response.data.data)
? response.data.data.map(transformChannel)
: [];
return {
success: response.data.success,
message: response.data.message,
channels: response.data.data,
channels,
};
} catch (error) {
console.error("获取通知渠道失败:", error);
@@ -228,13 +358,17 @@ export const getNotificationTemplates = async (): Promise<{
const response = await api.get<{
success: boolean;
message?: string;
data?: NotificationTemplate[];
data?: BackendNotificationTemplate[];
}>("/api/notifications/templates");
const templates = Array.isArray(response.data.data)
? response.data.data.map(transformTemplate)
: [];
return {
success: response.data.success,
message: response.data.message,
templates: response.data.data,
templates,
};
} catch (error) {
console.error("获取通知模板失败:", error);
@@ -357,17 +491,17 @@ export const saveNotificationSettings = async (
// 创建通知渠道
export const createNotificationChannel = async (
channel: Omit<NotificationChannel, "id">
channel: Omit<NotificationChannel, "id" | "createdBy" | "createdAt" | "updatedAt">
): Promise<{
success: boolean;
message?: string;
channelId?: string;
channelId?: number;
}> => {
try {
const response = await api.post<{
success: boolean;
message?: string;
data?: { id: string };
data?: { id: number };
}>("/api/notifications/channels", channel);
return {
@@ -386,17 +520,25 @@ export const createNotificationChannel = async (
// 更新通知渠道
export const updateNotificationChannel = async (
id: string,
channel: Partial<NotificationChannel>
id: number,
channel: Partial<
Omit<NotificationChannel, "id" | "createdBy" | "createdAt" | "updatedAt">
>
): Promise<{
success: boolean;
message?: string;
}> => {
try {
const payload: Record<string, unknown> = {};
if (channel.name !== undefined) payload.name = channel.name;
if (channel.type !== undefined) payload.type = channel.type;
if (channel.config !== undefined) payload.config = channel.config;
if (channel.enabled !== undefined) payload.enabled = channel.enabled;
const response = await api.put<{
success: boolean;
message?: string;
}>(`/api/notifications/channels/${id}`, channel);
}>(`/api/notifications/channels/${id}`, payload);
return response.data;
} catch (error) {
@@ -410,7 +552,7 @@ export const updateNotificationChannel = async (
// 删除通知渠道
export const deleteNotificationChannel = async (
id: string
id: number
): Promise<{
success: boolean;
message?: string;
@@ -433,18 +575,26 @@ export const deleteNotificationChannel = async (
// 创建通知模板
export const createNotificationTemplate = async (
template: Omit<NotificationTemplate, "id">
template: Omit<NotificationTemplate, "id" | "createdBy" | "createdAt" | "updatedAt">
): Promise<{
success: boolean;
message?: string;
templateId?: string;
templateId?: number;
}> => {
try {
const payload = {
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
is_default: template.isDefault,
};
const response = await api.post<{
success: boolean;
message?: string;
data?: { id: string };
}>("/api/notifications/templates", template);
data?: { id: number };
}>("/api/notifications/templates", payload);
return {
success: response.data.success,
@@ -462,16 +612,27 @@ export const createNotificationTemplate = async (
// 更新通知模板
export const updateNotificationTemplate = async (
id: string,
template: Partial<NotificationTemplate>
id: number,
template: Partial<
Omit<NotificationTemplate, "id" | "createdBy" | "createdAt" | "updatedAt">
>
): Promise<{
success: boolean;
message?: string;
}> => {
try {
const response = await api.put(
const payload: Record<string, unknown> = {};
if (template.name !== undefined) payload.name = template.name;
if (template.type !== undefined) payload.type = template.type;
if (template.subject !== undefined) payload.subject = template.subject;
if (template.content !== undefined) payload.content = template.content;
if (template.isDefault !== undefined) {
payload.is_default = template.isDefault;
}
const response = await api.put<{ success: boolean; message?: string }>(
`/api/notifications/templates/${id}`,
template
payload
);
return response.data;
} catch (error) {
@@ -485,7 +646,7 @@ export const updateNotificationTemplate = async (
// 删除通知模板
export const deleteNotificationTemplate = async (
id: string
id: number
): Promise<{
success: boolean;
message?: string;
+2 -18
View File
@@ -3,23 +3,7 @@ import { Box, Flex, Text } from "@radix-ui/themes";
import { CheckIcon } from "@radix-ui/react-icons";
import { useTranslation } from "react-i18next";
import { createPortal } from "react-dom";
// 通知渠道类型定义
export interface NotificationChannel {
id: string;
name: string;
type: string;
config?: any;
enabled?: boolean;
}
// 组件属性接口
export interface ChannelSelectorProps {
channels: NotificationChannel[];
selectedChannelIds: string[];
onChange: (channelIds: string[]) => void;
placeholder?: string;
}
import { ChannelSelectorProps } from "@/types/components";
/**
* 通知渠道选择器组件
@@ -61,7 +45,7 @@ const ChannelSelector = ({
};
// 处理选项点击
const handleItemClick = (channelId: string) => {
const handleItemClick = (channelId: number) => {
// 切换选择状态
const isSelected = selectedChannelIds.includes(channelId);
const newSelectedIds = isSelected
+3 -2
View File
@@ -6,6 +6,7 @@ import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { getAgent, updateAgent } from "../../api/agents";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import type { Agent } from "../../types/agents";
const EditAgent = () => {
const navigate = useNavigate();
@@ -14,7 +15,7 @@ const EditAgent = () => {
const [notFound, setNotFound] = useState(false);
const [formData, setFormData] = useState<{
name: string;
status: "active" | "inactive" | "connecting" | "unknown";
status: Agent["status"];
}>({
name: "",
status: "inactive",
@@ -37,7 +38,7 @@ const EditAgent = () => {
setFormData({
name: agent.name,
status: agent.status || "inactive",
status: agent.status ?? "inactive",
});
} else {
setNotFound(true);
File diff suppressed because it is too large. Load diff
+14 -2
View File
@@ -7,7 +7,13 @@ export interface Agent {
name: string;
hostname?: string;
ip_addresses?: string;
status: "active" | "inactive" | "connecting" | "unknown";
status:
| "active"
| "inactive"
| "connecting"
| "unknown"
| "online"
| "offline";
version?: string;
os?: string;
created_at: string;
@@ -20,7 +26,13 @@ export interface AgentWithLatestMetrics {
name: string;
hostname?: string;
ip_addresses?: string;
status: "active" | "inactive" | "connecting" | "unknown";
status:
| "active"
| "inactive"
| "connecting"
| "unknown"
| "online"
| "offline";
version?: string;
os?: string;
created_at: string;
+14 -4
View File
@@ -1,24 +1,34 @@
/**
* 组件相关类型定义
*/
import { NotificationChannel } from "./notification";
import { Monitor } from "./monitors";
import { Agent, MetricHistory } from "./agents";
import { NotificationChannel } from "./notification";
// ChannelSelector 组件类型
export interface ChannelSelectorProps {
channels: NotificationChannel[];
selectedChannelIds: string[];
onChange: (channelIds: string[]) => void;
selectedChannelIds: number[];
onChange: (channelIds: number[]) => void;
placeholder?: string;
}
// StatusSummaryCard 组件类型
export interface StatusItem {
id: string;
name: string;
status: "up" | "down" | "pending" | "unknown" | "active" | "inactive";
status:
| "up"
| "down"
| "pending"
| "unknown"
| "active"
| "inactive"
| "online"
| "offline"
| "error";
time?: string;
}
+2 -2
View File
@@ -7,12 +7,12 @@ export interface Monitor {
name: string;
url: string;
method: string;
headers?: string;
headers?: Record<string, string> | string | null;
body?: string;
expected_status?: number;
interval: number;
timeout: number;
status: "up" | "down" | "pending" | "unknown";
status: "up" | "down" | "pending" | "unknown" | "error";
last_checked?: string;
response_time?: number;
user_id: number;
+13 -15
View File
@@ -3,28 +3,26 @@ export interface NotificationChannel {
id: number;
name: string;
type: string; // telegram, resend, feishu, wecom
config: string; // JSON字符串
config: Record<string, unknown>;
enabled: boolean;
created_by: number;
created_at: string;
updated_at: string;
createdBy?: number;
createdAt?: string;
updatedAt?: string;
}
// 通知模板类型定义
export interface NotificationTemplate {
id: number;
name: string;
type: string; // default, custom
type: string; // monitor, agent
subject: string;
content: string;
is_default: boolean;
created_by: number;
created_at: string;
updated_at: string;
isDefault: boolean;
createdBy?: number;
createdAt?: string;
updatedAt?: string;
}
// 前端接口所需的通知配置类型
export interface NotificationConfig {
channels: NotificationChannel[];
@@ -34,7 +32,7 @@ export interface NotificationConfig {
enabled: boolean;
onDown: boolean;
onRecovery: boolean;
channels: string[];
channels: number[];
};
agents: {
enabled: boolean;
@@ -46,14 +44,14 @@ export interface NotificationConfig {
memoryThreshold: number;
onDiskThreshold: boolean;
diskThreshold: number;
channels: string[];
channels: number[];
};
specificMonitors: {
[monitorId: string]: {
enabled: boolean;
onDown: boolean;
onRecovery: boolean;
channels: string[];
channels: number[];
};
};
specificAgents: {
@@ -67,7 +65,7 @@ export interface NotificationConfig {
memoryThreshold: number;
onDiskThreshold: boolean;
diskThreshold: number;
channels: string[];
channels: number[];
};
};
};
+2621 -39
View File
File diff suppressed because it is too large. Load diff