Merge pull request #45 from zaunist/feat/use_drizzle

feat: add drizzle
This commit is contained in:
zaunist authored and GitHub committed 2025-06-01 05:06:48 +08:00
commit 05288f9137
58 files changed
+4114 -4471

No files matched your search

+1
View File
@@ -75,6 +75,7 @@ coverage/
# Environment configuration files (DO NOT commit sensitive info!)
.env
.env*.local
/backend/.env
# --- IDE / Editor Configuration ---
# Files and directories generated by IDEs and editors
+4
View File
@@ -16,6 +16,7 @@ import (
"github.com/shirou/gopsutil/v3/net"
"github.com/xugou/agent/pkg/config"
"github.com/xugou/agent/pkg/model"
"github.com/xugou/agent/pkg/utils"
)
// Collector 定义数据收集器接口
@@ -52,6 +53,9 @@ func (c *DefaultCollector) Collect(ctx context.Context) (*model.SystemInfo, erro
// 设置操作系统版本,格式化为更有意义的信息
info.Version = fmt.Sprintf("%s %s (%s)", hostInfo.Platform, hostInfo.PlatformVersion, hostInfo.KernelVersion)
// 获取本地IP地址
info.IPAddresses = utils.GetLocalIPs()
// 获取CPU信息
cpuPercent, err := cpu.Percent(time.Second, false)
if err != nil {
+2 -1
View File
@@ -9,7 +9,8 @@ type SystemInfo struct {
Hostname string `json:"hostname"`
Platform string `json:"platform"`
OS string `json:"os"`
Version string `json:"version"` // 操作系统版本
Version string `json:"version"` // 操作系统版本
IPAddresses []string `json:"ip_addresses"` // IP地址列表
Keepalive int `json:"keepalive"`
CPUInfo CPUInfo `json:"cpu"`
MemoryInfo MemoryInfo `json:"memory"`
+11
View File
@@ -0,0 +1,11 @@
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
driver: 'd1-http',
strict: false,
verbose: true,
});
@@ -0,0 +1,188 @@
CREATE TABLE `agent_metrics_24h` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`agent_id` integer NOT NULL,
`timestamp` text DEFAULT 'CURRENT_TIMESTAMP',
`cpu_usage` real,
`cpu_cores` integer,
`cpu_model` text,
`memory_total` integer,
`memory_used` integer,
`memory_free` integer,
`memory_usage_rate` real,
`load_1` real,
`load_5` real,
`load_15` real,
`disk_metrics` text,
`network_metrics` text,
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `agents` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`token` text NOT NULL,
`created_by` integer NOT NULL,
`status` text DEFAULT 'inactive',
`created_at` text NOT NULL,
`updated_at` text NOT NULL,
`hostname` text,
`ip_addresses` text,
`os` text,
`version` text,
`keepalive` text,
`cpu_usage` real,
`memory_total` integer,
`memory_used` integer,
`disk_total` integer,
`disk_used` integer,
`network_rx` integer,
`network_tx` integer,
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `agents_token_unique` ON `agents` (`token`);--> statement-breakpoint
CREATE TABLE `monitor_daily_stats` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`monitor_id` integer NOT NULL,
`date` text NOT NULL,
`total_checks` integer DEFAULT 0 NOT NULL,
`up_checks` integer DEFAULT 0 NOT NULL,
`down_checks` integer DEFAULT 0 NOT NULL,
`avg_response_time` integer DEFAULT 0,
`min_response_time` integer DEFAULT 0,
`max_response_time` integer DEFAULT 0,
`availability` real DEFAULT 0,
`created_at` text NOT NULL,
FOREIGN KEY (`monitor_id`) REFERENCES `monitors`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `monitor_status_history_24h` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`monitor_id` integer NOT NULL,
`status` text NOT NULL,
`timestamp` text DEFAULT 'CURRENT_TIMESTAMP',
`response_time` integer,
`status_code` integer,
`error` text,
FOREIGN KEY (`monitor_id`) REFERENCES `monitors`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `monitors` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`url` text NOT NULL,
`method` text NOT NULL,
`interval` integer NOT NULL,
`timeout` integer NOT NULL,
`expected_status` integer NOT NULL,
`headers` text NOT NULL,
`body` text,
`created_by` integer NOT NULL,
`active` integer NOT NULL,
`status` text DEFAULT 'pending',
`response_time` integer DEFAULT 0,
`last_checked` text,
`created_at` text NOT NULL,
`updated_at` text NOT NULL,
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `notification_channels` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
`config` text NOT NULL,
`enabled` integer DEFAULT 1 NOT NULL,
`created_by` integer NOT NULL,
`created_at` text DEFAULT 'CURRENT_TIMESTAMP',
`updated_at` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `notification_history` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`type` text NOT NULL,
`target_id` integer,
`channel_id` integer NOT NULL,
`template_id` integer NOT NULL,
`status` text NOT NULL,
`content` text NOT NULL,
`error` text,
`sent_at` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (`channel_id`) REFERENCES `notification_channels`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`template_id`) REFERENCES `notification_templates`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `notification_settings` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`target_type` text DEFAULT 'global' NOT NULL,
`target_id` integer,
`enabled` integer DEFAULT 1 NOT NULL,
`on_down` integer DEFAULT 1 NOT NULL,
`on_recovery` integer DEFAULT 1 NOT NULL,
`on_offline` integer DEFAULT 1 NOT NULL,
`on_cpu_threshold` integer DEFAULT 0 NOT NULL,
`cpu_threshold` integer DEFAULT 90 NOT NULL,
`on_memory_threshold` integer DEFAULT 0 NOT NULL,
`memory_threshold` integer DEFAULT 85 NOT NULL,
`on_disk_threshold` integer DEFAULT 0 NOT NULL,
`disk_threshold` integer DEFAULT 90 NOT NULL,
`channels` text DEFAULT '[]',
`created_at` text DEFAULT 'CURRENT_TIMESTAMP',
`updated_at` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `notification_templates` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
`subject` text NOT NULL,
`content` text NOT NULL,
`is_default` integer DEFAULT 0 NOT NULL,
`created_by` integer NOT NULL,
`created_at` text DEFAULT 'CURRENT_TIMESTAMP',
`updated_at` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `status_page_agents` (
`config_id` integer NOT NULL,
`agent_id` integer NOT NULL,
PRIMARY KEY(`config_id`, `agent_id`),
FOREIGN KEY (`config_id`) REFERENCES `status_page_config`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`agent_id`) REFERENCES `agents`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `status_page_config` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`user_id` integer NOT NULL,
`title` text DEFAULT '系统状态' NOT NULL,
`description` text DEFAULT '系统当前运行状态',
`logo_url` text DEFAULT '',
`custom_css` text DEFAULT '',
`created_at` text DEFAULT 'CURRENT_TIMESTAMP',
`updated_at` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `status_page_monitors` (
`config_id` integer NOT NULL,
`monitor_id` integer NOT NULL,
PRIMARY KEY(`config_id`, `monitor_id`),
FOREIGN KEY (`config_id`) REFERENCES `status_page_config`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`monitor_id`) REFERENCES `monitors`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`username` text NOT NULL,
`password` text NOT NULL,
`email` text,
`role` text NOT NULL,
`created_at` text NOT NULL,
`updated_at` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_username_unique` ON `users` (`username`);
File diff suppressed because it is too large. Load diff
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1748308256007,
"tag": "0000_romantic_next_avengers",
"breakpoints": true
}
]
}
+626
View File
@@ -20,8 +20,11 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250317.0",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.1",
"nodemon": "^3.1.9",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",
"wrangler": "^4.13.1"
}
},
@@ -159,6 +162,13 @@
"node": ">=12"
}
},
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.3.1.tgz",
@@ -170,6 +180,442 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmmirror.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
"source-map-support": "^0.5.21"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/@esbuild-kit/esm-loader": {
"version": "2.6.5",
"resolved": "https://registry.npmmirror.com/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
"get-tsconfig": "^4.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.2",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
@@ -1204,6 +1650,13 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
@@ -1302,6 +1755,40 @@
"dev": true,
"license": "MIT"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
@@ -1355,6 +1842,22 @@
"node": ">=0.3.1"
}
},
"node_modules/drizzle-kit": {
"version": "0.31.1",
"resolved": "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.1.tgz",
"integrity": "sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"esbuild": "^0.25.2",
"esbuild-register": "^3.5.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1405,6 +1908,19 @@
"@esbuild/win32-x64": "0.25.2"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/exit-hook": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/exit-hook/-/exit-hook-2.2.1.tgz",
@@ -1464,6 +1980,19 @@
"source-map": "^0.6.1"
}
},
"node_modules/get-tsconfig": {
"version": "4.10.1",
"resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1564,6 +2093,13 @@
"node": ">=0.12.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
@@ -1793,6 +2329,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ -1847,6 +2393,16 @@
"node": ">=8.10.0"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1920,6 +2476,29 @@
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@@ -1954,6 +2533,17 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/stacktracey": {
"version": "2.1.8",
"resolved": "https://registry.npmmirror.com/stacktracey/-/stacktracey-2.1.8.tgz",
@@ -2064,6 +2654,26 @@
"license": "0BSD",
"optional": true
},
"node_modules/tsx": {
"version": "4.19.4",
"resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.19.4.tgz",
"integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.2.tgz",
@@ -2131,6 +2741,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/workerd": {
"version": "1.20250424.0",
"resolved": "https://registry.npmmirror.com/workerd/-/workerd-1.20250424.0.tgz",
+7 -3
View File
@@ -5,9 +5,10 @@
"type": "module",
"scripts": {
"dev": "wrangler dev --test-scheduled",
"build": "tsc",
"start": "node dist/index.js",
"deploy": "wrangler deploy"
"build": "tsc && npm run generate:migrations",
"deploy": "wrangler deploy",
"db:generate": "drizzle-kit generate",
"generate:migrations": "tsx scripts/generate-migrations.ts"
},
"keywords": [],
"author": "",
@@ -25,8 +26,11 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250317.0",
"cross-env": "^7.0.3",
"drizzle-kit": "^0.31.1",
"nodemon": "^3.1.9",
"ts-node": "^10.9.2",
"tsx": "^4.19.4",
"wrangler": "^4.13.1"
}
}
-310
View File
@@ -1,310 +0,0 @@
DROP TABLE IF EXISTS notification_history;
DROP TABLE IF EXISTS notification_settings;
DROP TABLE IF EXISTS notification_templates;
DROP TABLE IF EXISTS notification_channels;
DROP TABLE IF EXISTS status_page_agents;
DROP TABLE IF EXISTS status_page_monitors;
DROP TABLE IF EXISTS status_page_config;
DROP TABLE IF EXISTS monitor_status_history_24h;
DROP TABLE IF EXISTS monitor_daily_stats;
DROP TABLE IF EXISTS agent_metrics_24h;
DROP TABLE IF EXISTS agents;
DROP TABLE IF EXISTS monitors;
DROP TABLE IF EXISTS users;
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL DEFAULT 'viewer',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 监控表
CREATE TABLE IF NOT EXISTS monitors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
method TEXT NOT NULL DEFAULT 'GET',
interval INTEGER NOT NULL DEFAULT 60,
timeout INTEGER NOT NULL DEFAULT 30,
expected_status INTEGER NOT NULL DEFAULT 200,
headers TEXT DEFAULT '{}',
body TEXT DEFAULT '',
created_by INTEGER NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
status TEXT DEFAULT 'pending',
response_time INTEGER DEFAULT 0,
last_checked TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- 24小时监控状态历史表(热表)
CREATE TABLE IF NOT EXISTS monitor_status_history_24h (
id INTEGER PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
status TEXT NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
response_time INTEGER,
status_code INTEGER,
error TEXT,
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
);
-- 监控每日统计表
CREATE TABLE IF NOT EXISTS monitor_daily_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
monitor_id INTEGER NOT NULL,
date TEXT NOT NULL,
total_checks INTEGER NOT NULL DEFAULT 0,
up_checks INTEGER NOT NULL DEFAULT 0,
down_checks INTEGER NOT NULL DEFAULT 0,
avg_response_time INTEGER DEFAULT 0,
min_response_time INTEGER DEFAULT 0,
max_response_time INTEGER DEFAULT 0,
availability REAL DEFAULT 0,
FOREIGN KEY (monitor_id) REFERENCES monitors(id)
);
-- 客户端表
CREATE TABLE IF NOT EXISTS agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
created_by INTEGER NOT NULL,
status TEXT DEFAULT 'inactive',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
hostname TEXT,
ip_addresses TEXT, -- 存储多个IP地址的JSON字符串
os TEXT,
version TEXT,
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- 客户端资源指标表 24h
CREATE TABLE IF NOT EXISTS agent_metrics_24h (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id INTEGER NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- CPU指标
cpu_usage REAL, -- CPU使用率(%)
cpu_cores INTEGER, -- CPU核心数
cpu_model TEXT, -- CPU型号名称
-- 内存指标
memory_total BIGINT, -- 总内存(字节)
memory_used BIGINT, -- 已用内存(字节)
memory_free BIGINT, -- 空闲内存(字节)
memory_usage_rate REAL, -- 内存使用率(%)
-- 负载指标
load_1 REAL, -- 1分钟平均负载
load_5 REAL, -- 5分钟平均负载
load_15 REAL, -- 15分钟平均负载
-- 磁盘和网络指标(JSON格式存储)
disk_metrics TEXT, -- JSON格式存储多个磁盘信息
network_metrics TEXT, -- JSON格式存储多个网络接口信息
FOREIGN KEY (agent_id) REFERENCES agents(id)
);
-- 状态页配置表
CREATE TABLE IF NOT EXISTS status_page_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL DEFAULT '系统状态',
description TEXT DEFAULT '系统当前运行状态',
logo_url TEXT DEFAULT '',
custom_css TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- 状态页监控项关联表
CREATE TABLE IF NOT EXISTS status_page_monitors (
config_id INTEGER NOT NULL,
monitor_id INTEGER NOT NULL,
PRIMARY KEY (config_id, monitor_id),
FOREIGN KEY (config_id) REFERENCES status_page_config(id) ON DELETE CASCADE,
FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE
);
-- 状态页客户端关联表
CREATE TABLE IF NOT EXISTS status_page_agents (
config_id INTEGER NOT NULL,
agent_id INTEGER NOT NULL,
PRIMARY KEY (config_id, agent_id),
FOREIGN KEY (config_id) REFERENCES status_page_config(id) ON DELETE CASCADE,
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
);
-- 初始管理员用户 (密码: admin123)
INSERT OR IGNORE INTO users (id, username, password, role)
VALUES (1, 'admin', '$2a$10$cy8EjTIFgMXQfKrMV1lw6.Ltx6P/VVKCGP7PME3XbZv3lmDmTUwEK', 'admin');
-- 以下是通知系统表结构 --
-- 通知渠道表
CREATE TABLE IF NOT EXISTS notification_channels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL, -- email, telegram
config TEXT NOT NULL, -- JSON格式存储配置,如邮箱地址、API令牌等
enabled BOOLEAN NOT NULL DEFAULT 1,
created_by INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- 通知模板表
CREATE TABLE IF NOT EXISTS notification_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL, -- default, custom
subject TEXT NOT NULL, -- 邮件主题模板
content TEXT NOT NULL, -- 消息内容模板
is_default BOOLEAN NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
-- 统一通知设置表
CREATE TABLE IF NOT EXISTS notification_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
target_type TEXT NOT NULL DEFAULT 'global', -- global, monitor, agent
target_id INTEGER DEFAULT NULL, -- 当target_type不是global时,存储monitor_id或agent_id
enabled BOOLEAN NOT NULL DEFAULT 1,
on_down BOOLEAN NOT NULL DEFAULT 1, -- 适用于monitor
on_recovery BOOLEAN NOT NULL DEFAULT 1, -- 适用于monitor和agent
on_offline BOOLEAN NOT NULL DEFAULT 1, -- 适用于agent
on_cpu_threshold BOOLEAN NOT NULL DEFAULT 0, -- 适用于agent
cpu_threshold INTEGER NOT NULL DEFAULT 90, -- 适用于agent
on_memory_threshold BOOLEAN NOT NULL DEFAULT 0, -- 适用于agent
memory_threshold INTEGER NOT NULL DEFAULT 85, -- 适用于agent
on_disk_threshold BOOLEAN NOT NULL DEFAULT 0, -- 适用于agent
disk_threshold INTEGER NOT NULL DEFAULT 90, -- 适用于agent
channels TEXT DEFAULT '[]', -- JSON数组,存储channel IDs
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, target_type, target_id)
);
-- 通知历史记录表
CREATE TABLE IF NOT EXISTS notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL, -- monitor, agent, system
target_id INTEGER, -- monitor_id 或 agent_id,系统通知为null
channel_id INTEGER NOT NULL,
template_id INTEGER NOT NULL,
status TEXT NOT NULL, -- success, failed, pending
content TEXT NOT NULL,
error TEXT,
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES notification_channels(id),
FOREIGN KEY (template_id) REFERENCES notification_templates(id)
);
-- 初始通知模板
INSERT OR IGNORE INTO notification_templates (id, name, type, subject, content, is_default, created_by)
VALUES (
1,
'Monitor监控模板',
'monitor',
'【${status}】${name} 监控状态变更',
'🔔 网站监控状态变更通知
📊 服务: ${name}
🔄 状态: ${status} (之前: ${previous_status})
🕒 时间: ${time}
🔗 地址: ${url}
⏱️ 响应时间: ${response_time}
📝 实际状态码: ${status_code}
🎯 期望状态码: ${expected_status}
❗ 错误信息: ${error}',
1,
1
);
-- Agent客户端监控模板
INSERT OR IGNORE INTO notification_templates (id, name, type, subject, content, is_default, created_by)
VALUES (
2,
'Agent监控模板',
'agent',
'【${status}】${name} 客户端状态变更',
'🔔 客户端状态变更通知
📊 主机: ${name}
🔄 状态: ${status} (之前: ${previous_status})
🕒 时间: ${time}
🖥️ 主机信息:
主机名: ${hostname}
IP地址: ${ip_addresses}
操作系统: ${os}
❗ 错误信息: ${error}',
1,
1
);
-- 初始通知渠道
INSERT OR IGNORE INTO notification_channels (id, name, type, config, enabled, created_by)
VALUES (
1,
'默认Telegram通知渠道(https://t.me/xugou_group)',
'telegram',
'{"botToken": "8163201319:AAGyY7FtdaRb6o8NCVXSbBUb6ofDK45cNJU", "chatId": "-1002608818360"}',
1,
1
);
-- 初始全局监控通知设置
INSERT OR IGNORE INTO notification_settings (
id, user_id, target_type,
enabled, on_down, on_recovery,
channels
)
VALUES (
1, 1, 'global-monitor',
1, 1, 1,
'[1]'
);
-- 初始全局客户端通知设置
INSERT OR IGNORE INTO notification_settings (
id, user_id, target_type,
enabled, on_offline, on_recovery,
on_cpu_threshold, cpu_threshold,
on_memory_threshold, memory_threshold,
on_disk_threshold, disk_threshold,
channels
)
VALUES (
2, 1, 'global-agent',
1, 1, 1,
1, 80,
1, 80,
1, 90,
'[1]'
);
+91
View File
@@ -0,0 +1,91 @@
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
// ESM 模块中获取 __dirname 的替代方案
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 添加 IF NOT EXISTS 到各种 SQL 语句
function addIfNotExists(sql: string): string {
return sql
// 处理 CREATE TABLE 语句
.replace(/CREATE TABLE (`?\w+`?)/g, 'CREATE TABLE IF NOT EXISTS $1')
// 处理 CREATE INDEX 语句
.replace(/CREATE (UNIQUE )?INDEX (`?\w+`?)/g, 'CREATE $1INDEX IF NOT EXISTS $2')
// 处理 ALTER TABLE ADD COLUMN 语句
.replace(
/ALTER TABLE (`?\w+`?) ADD (COLUMN )?(`?\w+`? \w+.*)/g,
(match, table, _, column) => {
// 提取列名(去掉类型和约束)
const columnName = column.match(/^`?\w+`?/)[0];
return `
-- 检查列是否存在
SELECT CASE
WHEN EXISTS (
SELECT 1 FROM pragma_table_info(${table}) WHERE name = ${columnName}
)
THEN 1
ELSE (
ALTER TABLE ${table} ADD ${column}
)
END;`;
}
);
}
// 生成迁移文件
async function generateMigrations() {
const migrationsDir = path.join(__dirname, '..', 'drizzle');
const outputFile = path.join(__dirname, '..', 'src', 'db', 'generated-migrations.ts');
// 确保目录存在
const outputDir = path.dirname(outputFile);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 读取所有 SQL 文件
const files = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'))
.sort();
// 生成迁移数组
const migrations = files.map(file => {
const content = fs.readFileSync(path.join(migrationsDir, file), 'utf-8');
// 对每个 SQL 语句添加 IF NOT EXISTS
const modifiedContent = content
.split('--> statement-breakpoint')
.map(stmt => addIfNotExists(stmt.trim()))
.filter(stmt => stmt.length > 0)
.join('\n--> statement-breakpoint\n');
return {
name: file,
content: modifiedContent.replace(/`/g, '\\`') // 转义反引号
};
});
// 生成 TypeScript 文件内容
const fileContent = `// 此文件由 generate-migrations.ts 自动生成
// 请不要手动修改
export interface Migration {
name: string;
sql: string;
}
export const MIGRATIONS: Migration[] = [
${migrations.map(m => ` {
name: "${m.name}",
sql: \`${m.content}\`
}`).join(',\n')}
];
`;
// 写入文件
fs.writeFileSync(outputFile, fileContent, 'utf-8');
console.log(`已生成迁移文件: ${outputFile}`);
}
generateMigrations().catch(console.error);
+53 -59
View File
@@ -1,10 +1,9 @@
import { Hono } from "hono";
import { Bindings } from "../models/db";
import { Agent, Metrics } from "../models/agent";
import { Agent } from "../models/agent";
import {
getAgents,
getAgentDetail,
createAgentService,
updateAgentService,
deleteAgentService,
generateAgentToken,
@@ -21,7 +20,7 @@ const agents = new Hono<{
// 获取所有客户端
agents.get("/", async (c) => {
const result = await getAgents(c.env.DB);
const result = await getAgents();
return c.json(
{
@@ -33,23 +32,6 @@ agents.get("/", async (c) => {
);
});
// 创建新客户端
agents.post("/", async (c) => {
const { name } = await c.req.json();
const payload = c.get("jwtPayload");
const result = await createAgentService(c.env.DB, c.env, name, payload.id);
return c.json(
{
success: result.success,
message: result.message,
agent: result.agent,
},
result.status as any
);
});
// 更新客户端信息
agents.put("/:id", async (c) => {
const agentId = Number(c.req.param("id"));
@@ -70,18 +52,27 @@ agents.put("/:id", async (c) => {
// 删除客户端
agents.delete("/:id", async (c) => {
const agentId = Number(c.req.param("id"));
const payload = c.get("jwtPayload");
try {
const agentId = Number(c.req.param("id"));
const result = await deleteAgentService(c.env.DB, agentId);
await deleteAgentService(agentId);
return c.json(
{
success: result.success,
message: result.message,
},
result.status as any
);
return c.json(
{
success: true,
message: "客户端已删除",
},
200
);
} catch (error) {
return c.json(
{
success: false,
message: error instanceof Error ? error.message : String(error),
},
500
);
}
});
// 生成客户端Token
@@ -101,18 +92,9 @@ agents.post("/token/generate", async (c) => {
// 客户端自注册接口
agents.post("/register", async (c) => {
const { token, name, hostname, ip_addresses, os, version } =
await c.req.json();
console.log("token", token);
console.log("name", name);
console.log("hostname", hostname);
console.log("ip_addresses", ip_addresses);
console.log("os", os);
console.log("version", version);
const { token, name, hostname, ip_addresses, os, version } = await c.req.json();
const result = await registerAgentService(
c.env.DB,
c.env,
token,
name || "New Agent",
@@ -137,25 +119,36 @@ agents.post("/status", async (c) => {
// 获取客户端发送的所有数据并打印日志
const statusData = await c.req.json();
const result = await updateAgentStatusService(c.env.DB, c.env, statusData);
console.log("statusData: ", statusData);
return c.json(
{
success: result.success,
message: result.message,
},
result.status as any
);
try {
await updateAgentStatusService(statusData);
return c.json(
{
success: true,
message: "客户端状态已更新",
},
200
);
} catch (error) {
return c.json(
{
success: false,
message: error instanceof Error ? error.message : String(error),
},
500
);
}
});
// 获取单个客户端的指标
agents.get("/:id/metrics", async (c) => {
const agentId = Number(c.req.param("id"));
const result = await getAgentMetrics(c.env.DB, agentId);
const result = await getAgentMetrics(agentId);
return c.json(
{
success: result.success,
agent: result.results,
success: true,
agent: result,
message: "获取客户端指标成功",
},
200
@@ -165,11 +158,11 @@ agents.get("/:id/metrics", async (c) => {
// 获取单个客户端的最新指标
agents.get("/:id/metrics/latest", async (c) => {
const agentId = Number(c.req.param("id"));
const result = await getLatestAgentMetrics(c.env.DB, agentId);
const result = await getLatestAgentMetrics(agentId);
return c.json(
{
success: result.success,
agent: result.results,
success: true,
agent: result,
message: "获取客户端最新指标成功",
},
200
@@ -180,15 +173,16 @@ agents.get("/:id/metrics/latest", async (c) => {
agents.get("/:id", async (c) => {
const agentId = Number(c.req.param("id"));
const result = await getAgentDetail(c.env.DB, agentId);
const result = await getAgentDetail(agentId);
console.log("result: ", result);
return c.json(
{
success: result.success,
agent: result.agent,
message: result.message,
success: true,
agent: result,
},
result.status as any
200
);
});
+2 -39
View File
@@ -5,46 +5,9 @@ import {
registerUser,
getCurrentUser,
} from "../services/AuthService";
import { VersionMetadata } from "../models/db";
import { Bindings } from "../models/db";
import { db } from "../config";
// 导入 D1 数据库类型
type Bindings = {
DB: D1Database;
CF_VERSION_METADATA?: VersionMetadata;
};
interface D1Database {
prepare(query: string): D1PreparedStatement;
dump(): Promise<ArrayBuffer>;
batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
exec<T = unknown>(query: string): Promise<D1Result<T>>;
}
interface D1PreparedStatement {
bind(...values: any[]): D1PreparedStatement;
first<T = unknown>(colName?: string): Promise<T>;
run<T = unknown>(): Promise<D1Result<T>>;
all<T = unknown>(): Promise<D1Result<T>>;
raw<T = unknown>(): Promise<T[]>;
}
interface D1Result<T = unknown> {
results?: T[];
success: boolean;
error?: string;
meta?: object;
}
// 用户类型定义
interface User {
id: number;
username: string;
password: string;
email?: string;
role: string;
created_at?: string;
updated_at?: string;
}
const auth = new Hono<{ Bindings: Bindings }>();
+1 -1
View File
@@ -5,7 +5,7 @@ export const dashboard = new Hono<{ Bindings: Bindings }>();
// 获取仪表盘数据
dashboard.get("/", async (c) => {
const result = await getDashboardData(c.env.DB);
const result = await getDashboardData();
return c.json(
{
monitors: result.monitors,
+9 -12
View File
@@ -9,7 +9,7 @@ monitors.get("/", async (c) => {
const payload = c.get("jwtPayload");
// 调用服务层获取监控列表
const result = await MonitorService.getAllMonitors(c.env.DB);
const result = await MonitorService.getAllMonitors();
return c.json(
{
@@ -23,7 +23,7 @@ monitors.get("/", async (c) => {
// 获取所有监控的每日统计数据
monitors.get("/daily", async (c) => {
// 调用服务层获取所有监控的每日统计数据
const result = await MonitorService.getAllMonitorDailyStats(c.env.DB);
const result = await MonitorService.getAllMonitorDailyStats();
return c.json({
success: result.success,
@@ -38,7 +38,7 @@ monitors.post("/", async (c) => {
const data = await c.req.json();
// 调用服务层创建监控
const result = await MonitorService.createMonitor(c.env.DB, data, payload.id);
const result = await MonitorService.createMonitor(data, payload.id);
return c.json(
{
@@ -53,7 +53,7 @@ monitors.post("/", async (c) => {
// 获取所有监控状态历史
monitors.get("/history", async (c) => {
// 调用服务层获取监控历史
const result = await MonitorService.getAllMonitorStatusHistory(c.env.DB);
const result = await MonitorService.getAllMonitorStatusHistory();
return c.json(
{
@@ -70,7 +70,7 @@ monitors.get("/:id", async (c) => {
const payload = c.get("jwtPayload");
// 调用服务层获取监控详情
const result = await MonitorService.getMonitorById(c.env.DB, id);
const result = await MonitorService.getMonitorById(id);
return c.json(
{
@@ -85,11 +85,10 @@ monitors.get("/:id", async (c) => {
// 更新监控
monitors.put("/:id", async (c) => {
const id = parseInt(c.req.param("id"));
const payload = c.get("jwtPayload");
const data = await c.req.json();
// 调用服务层更新监控
const result = await MonitorService.updateMonitor(c.env.DB, id, data);
const result = await MonitorService.updateMonitor(id, data);
return c.json(
{
@@ -107,7 +106,7 @@ monitors.delete("/:id", async (c) => {
const payload = c.get("jwtPayload");
// 调用服务层删除监控
const result = await MonitorService.deleteMonitor(c.env.DB, id);
const result = await MonitorService.deleteMonitor(id);
return c.json(
{
@@ -125,7 +124,6 @@ monitors.get("/:id/history", async (c) => {
// 调用服务层获取监控历史
const result = await MonitorService.getMonitorStatusHistoryById(
c.env.DB,
id,
payload.id,
payload.role
@@ -146,7 +144,7 @@ monitors.get("/:id/daily", async (c) => {
const id = parseInt(c.req.param("id"));
// 调用服务层获取每日统计数据
const result = await MonitorService.getMonitorDailyStats(c.env.DB, id);
const result = await MonitorService.getMonitorDailyStats(id);
return c.json({
success: result.success,
@@ -158,10 +156,9 @@ monitors.get("/:id/daily", async (c) => {
// 手动检查单个监控
monitors.post("/:id/check", async (c) => {
const id = parseInt(c.req.param("id"));
const payload = c.get("jwtPayload");
// 调用服务层手动检查监控
const result = await MonitorService.manualCheckMonitor(c.env.DB, id, c.env);
const result = await MonitorService.manualCheckMonitor(id);
return c.json(
{
+12 -33
View File
@@ -8,9 +8,7 @@ const notifications = new Hono<{ Bindings: Bindings }>();
// 获取通知配置
notifications.get("/", async (c) => {
try {
const db = c.env.DB;
const config = await NotificationService.getNotificationConfig(db);
const config = await NotificationService.getNotificationConfig();
return c.json({
success: true,
@@ -32,8 +30,7 @@ notifications.get("/", async (c) => {
// 获取通知渠道列表
notifications.get("/channels", async (c) => {
try {
const db = c.env.DB;
const channels = await NotificationService.getNotificationChannels(db);
const channels = await NotificationService.getNotificationChannels();
return c.json({
success: true,
@@ -55,7 +52,6 @@ notifications.get("/channels", async (c) => {
// 获取单个通知渠道
notifications.get("/channels/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -68,10 +64,7 @@ notifications.get("/channels/:id", async (c) => {
);
}
const channel = await NotificationService.getNotificationChannelById(
db,
id
);
const channel = await NotificationService.getNotificationChannelById(id);
if (!channel) {
return c.json(
@@ -118,7 +111,7 @@ notifications.post("/channels", async (c) => {
const validatedData = schema.parse(body);
// 创建渠道
const result = await NotificationService.createNotificationChannel(db, {
const result = await NotificationService.createNotificationChannel({
name: validatedData.name,
type: validatedData.type,
config: validatedData.config,
@@ -163,7 +156,6 @@ notifications.post("/channels", async (c) => {
// 更新通知渠道
notifications.put("/channels/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -190,7 +182,6 @@ notifications.put("/channels/:id", async (c) => {
// 更新渠道
const result = await NotificationService.updateNotificationChannel(
db,
id,
validatedData
);
@@ -225,7 +216,6 @@ notifications.put("/channels/:id", async (c) => {
// 删除通知渠道
notifications.delete("/channels/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -238,7 +228,7 @@ notifications.delete("/channels/:id", async (c) => {
);
}
const result = await NotificationService.deleteNotificationChannel(db, id);
const result = await NotificationService.deleteNotificationChannel(id);
if (!result.success) {
return c.json(
@@ -270,8 +260,7 @@ notifications.delete("/channels/:id", async (c) => {
// 获取通知模板列表
notifications.get("/templates", async (c) => {
try {
const db = c.env.DB;
const templates = await NotificationService.getNotificationTemplates(db);
const templates = await NotificationService.getNotificationTemplates();
return c.json({
success: true,
@@ -293,7 +282,6 @@ notifications.get("/templates", async (c) => {
// 获取单个通知模板
notifications.get("/templates/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -306,10 +294,7 @@ notifications.get("/templates/:id", async (c) => {
);
}
const template = await NotificationService.getNotificationTemplateById(
db,
id
);
const template = await NotificationService.getNotificationTemplateById(id);
if (!template) {
return c.json(
@@ -341,7 +326,6 @@ notifications.get("/templates/:id", async (c) => {
// 创建通知模板
notifications.post("/templates", async (c) => {
try {
const db = c.env.DB;
const userId = c.get("jwtPayload").id;
const body = await c.req.json();
@@ -357,7 +341,7 @@ notifications.post("/templates", async (c) => {
const validatedData = schema.parse(body);
// 创建模板
const result = await NotificationService.createNotificationTemplate(db, {
const result = await NotificationService.createNotificationTemplate({
name: validatedData.name,
type: validatedData.type,
subject: validatedData.subject,
@@ -405,7 +389,6 @@ notifications.post("/templates", async (c) => {
// 更新通知模板
notifications.put("/templates/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -433,7 +416,6 @@ notifications.put("/templates/:id", async (c) => {
// 更新模板
const result = await NotificationService.updateNotificationTemplate(
db,
id,
validatedData
);
@@ -468,7 +450,6 @@ notifications.put("/templates/:id", async (c) => {
// 删除通知模板
notifications.delete("/templates/:id", async (c) => {
try {
const db = c.env.DB;
const id = parseInt(c.req.param("id"));
if (isNaN(id)) {
@@ -481,7 +462,7 @@ notifications.delete("/templates/:id", async (c) => {
);
}
const result = await NotificationService.deleteNotificationTemplate(db, id);
const result = await NotificationService.deleteNotificationTemplate(id);
if (!result.success) {
return c.json(
@@ -513,7 +494,6 @@ notifications.delete("/templates/:id", async (c) => {
// 保存通知设置
notifications.post("/settings", async (c) => {
try {
const db = c.env.DB;
const userId = c.get("jwtPayload").id;
const body = await c.req.json();
@@ -542,10 +522,10 @@ notifications.post("/settings", async (c) => {
: JSON.stringify(validatedData.channels);
// 保存设置
const result = await NotificationService.createOrUpdateSettings(db, {
const result = await NotificationService.createOrUpdateSettings({
user_id: userId,
target_type: validatedData.target_type,
target_id: validatedData.target_id || null,
target_id: validatedData.target_id || 0,
enabled: validatedData.enabled,
on_down: validatedData.on_down || false,
on_recovery: validatedData.on_recovery || false,
@@ -590,7 +570,6 @@ notifications.post("/settings", async (c) => {
// 获取通知历史记录
notifications.get("/history", async (c) => {
try {
const db = c.env.DB;
const type = c.req.query("type") || "";
const target_id = c.req.query("target_id")
? parseInt(c.req.query("target_id")!)
@@ -600,7 +579,7 @@ notifications.get("/history", async (c) => {
const page = c.req.query("page") ? parseInt(c.req.query("page")!) : 1;
const offset = (page - 1) * limit;
const result = await NotificationService.getNotificationHistory(db, {
const result = await NotificationService.getNotificationHistory({
type,
targetId: target_id,
status,
+3 -3
View File
@@ -25,7 +25,7 @@ status.get("/config", async (c) => {
const userId = payload.id;
try {
const config = await getStatusPageConfig(c.env, userId);
const config = await getStatusPageConfig(userId);
return c.json(config);
} catch (error) {
console.error("获取状态页配置失败:", error);
@@ -47,7 +47,7 @@ status.post("/config", async (c) => {
}
try {
const result = await saveStatusPageConfig(c.env, userId, data);
const result = await saveStatusPageConfig(userId, data);
return c.json(result);
} catch (error) {
console.error("保存状态页配置失败:", error);
@@ -56,7 +56,7 @@ status.post("/config", async (c) => {
});
status.get("/data", async (c) => {
const result = await getStatusPagePublicData(c.env);
const result = await getStatusPagePublicData();
return c.json(result);
});
+4 -11
View File
@@ -16,7 +16,7 @@ users.get("/", async (c) => {
try {
const payload = c.get("jwtPayload");
const result = await getAllUsersService(c.env, payload.role);
const result = await getAllUsersService(payload.role);
return c.json(
{ success: result.success, users: result.users, message: result.message },
@@ -34,12 +34,7 @@ users.get("/:id", async (c) => {
const id = parseInt(c.req.param("id"));
const payload = c.get("jwtPayload");
const result = await getUserByIdService(
c.env,
id,
payload.id,
payload.role
);
const result = await getUserByIdService(id, payload.id, payload.role);
return c.json(
{ success: result.success, user: result.user, message: result.message },
@@ -57,7 +52,7 @@ users.post("/", async (c) => {
const payload = c.get("jwtPayload");
const userData = await c.req.json();
const result = await createUserService(c.env, userData, payload.role);
const result = await createUserService(userData, payload.role);
return c.json(
{ success: result.success, user: result.user, message: result.message },
@@ -77,7 +72,6 @@ users.put("/:id", async (c) => {
const updateData = await c.req.json();
const result = await updateUserService(
c.env,
id,
updateData,
payload.id,
@@ -100,7 +94,7 @@ users.delete("/:id", async (c) => {
const id = parseInt(c.req.param("id"));
const payload = c.get("jwtPayload");
const result = await deleteUserService(c.env, id, payload.id, payload.role);
const result = await deleteUserService(id, payload.id, payload.role);
return c.json(
{ success: result.success, message: result.message },
@@ -120,7 +114,6 @@ users.post("/:id/change-password", async (c) => {
const passwordData = await c.req.json();
const result = await changePasswordService(
c.env,
id,
passwordData,
payload.id,
+18
View File
@@ -0,0 +1,18 @@
import { drizzle } from "drizzle-orm/d1";
import { Bindings } from "../models/db";
import * as schema from "../db/schema";
let db: any;
export function initDb(env: Bindings) {
db = drizzle(
env.DB,
{
schema: {
...schema
}
}
);
}
export { db };
+1
View File
@@ -0,0 +1 @@
export * from "./db";
+202
View File
@@ -0,0 +1,202 @@
// 此文件由 generate-migrations.ts 自动生成
// 请不要手动修改
export interface Migration {
name: string;
sql: string;
}
export const MIGRATIONS: Migration[] = [
{
name: "0000_romantic_next_avengers.sql",
sql: `CREATE TABLE IF NOT EXISTS \`agent_metrics_24h\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`agent_id\` integer NOT NULL,
\`timestamp\` text DEFAULT 'CURRENT_TIMESTAMP',
\`cpu_usage\` real,
\`cpu_cores\` integer,
\`cpu_model\` text,
\`memory_total\` integer,
\`memory_used\` integer,
\`memory_free\` integer,
\`memory_usage_rate\` real,
\`load_1\` real,
\`load_5\` real,
\`load_15\` real,
\`disk_metrics\` text,
\`network_metrics\` text,
FOREIGN KEY (\`agent_id\`) REFERENCES \`agents\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`agents\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`name\` text NOT NULL,
\`token\` text NOT NULL,
\`created_by\` integer NOT NULL,
\`status\` text DEFAULT 'inactive',
\`created_at\` text NOT NULL,
\`updated_at\` text NOT NULL,
\`hostname\` text,
\`ip_addresses\` text,
\`os\` text,
\`version\` text,
\`keepalive\` text,
\`cpu_usage\` real,
\`memory_total\` integer,
\`memory_used\` integer,
\`disk_total\` integer,
\`disk_used\` integer,
\`network_rx\` integer,
\`network_tx\` integer,
FOREIGN KEY (\`created_by\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS \`agents_token_unique\` ON \`agents\` (\`token\`);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`monitor_daily_stats\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`monitor_id\` integer NOT NULL,
\`date\` text NOT NULL,
\`total_checks\` integer DEFAULT 0 NOT NULL,
\`up_checks\` integer DEFAULT 0 NOT NULL,
\`down_checks\` integer DEFAULT 0 NOT NULL,
\`avg_response_time\` integer DEFAULT 0,
\`min_response_time\` integer DEFAULT 0,
\`max_response_time\` integer DEFAULT 0,
\`availability\` real DEFAULT 0,
\`created_at\` text NOT NULL,
FOREIGN KEY (\`monitor_id\`) REFERENCES \`monitors\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`monitor_status_history_24h\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`monitor_id\` integer NOT NULL,
\`status\` text NOT NULL,
\`timestamp\` text DEFAULT 'CURRENT_TIMESTAMP',
\`response_time\` integer,
\`status_code\` integer,
\`error\` text,
FOREIGN KEY (\`monitor_id\`) REFERENCES \`monitors\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`monitors\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`name\` text NOT NULL,
\`url\` text NOT NULL,
\`method\` text NOT NULL,
\`interval\` integer NOT NULL,
\`timeout\` integer NOT NULL,
\`expected_status\` integer NOT NULL,
\`headers\` text NOT NULL,
\`body\` text,
\`created_by\` integer NOT NULL,
\`active\` integer NOT NULL,
\`status\` text DEFAULT 'pending',
\`response_time\` integer DEFAULT 0,
\`last_checked\` text,
\`created_at\` text NOT NULL,
\`updated_at\` text NOT NULL,
FOREIGN KEY (\`created_by\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`notification_channels\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`name\` text NOT NULL,
\`type\` text NOT NULL,
\`config\` text NOT NULL,
\`enabled\` integer DEFAULT 1 NOT NULL,
\`created_by\` integer NOT NULL,
\`created_at\` text DEFAULT 'CURRENT_TIMESTAMP',
\`updated_at\` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (\`created_by\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`notification_history\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`type\` text NOT NULL,
\`target_id\` integer,
\`channel_id\` integer NOT NULL,
\`template_id\` integer NOT NULL,
\`status\` text NOT NULL,
\`content\` text NOT NULL,
\`error\` text,
\`sent_at\` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (\`channel_id\`) REFERENCES \`notification_channels\`(\`id\`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (\`template_id\`) REFERENCES \`notification_templates\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`notification_settings\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`user_id\` integer NOT NULL,
\`target_type\` text DEFAULT 'global' NOT NULL,
\`target_id\` integer,
\`enabled\` integer DEFAULT 1 NOT NULL,
\`on_down\` integer DEFAULT 1 NOT NULL,
\`on_recovery\` integer DEFAULT 1 NOT NULL,
\`on_offline\` integer DEFAULT 1 NOT NULL,
\`on_cpu_threshold\` integer DEFAULT 0 NOT NULL,
\`cpu_threshold\` integer DEFAULT 90 NOT NULL,
\`on_memory_threshold\` integer DEFAULT 0 NOT NULL,
\`memory_threshold\` integer DEFAULT 85 NOT NULL,
\`on_disk_threshold\` integer DEFAULT 0 NOT NULL,
\`disk_threshold\` integer DEFAULT 90 NOT NULL,
\`channels\` text DEFAULT '[]',
\`created_at\` text DEFAULT 'CURRENT_TIMESTAMP',
\`updated_at\` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`notification_templates\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`name\` text NOT NULL,
\`type\` text NOT NULL,
\`subject\` text NOT NULL,
\`content\` text NOT NULL,
\`is_default\` integer DEFAULT 0 NOT NULL,
\`created_by\` integer NOT NULL,
\`created_at\` text DEFAULT 'CURRENT_TIMESTAMP',
\`updated_at\` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (\`created_by\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`status_page_agents\` (
\`config_id\` integer NOT NULL,
\`agent_id\` integer NOT NULL,
PRIMARY KEY(\`config_id\`, \`agent_id\`),
FOREIGN KEY (\`config_id\`) REFERENCES \`status_page_config\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`agent_id\`) REFERENCES \`agents\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`status_page_config\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`user_id\` integer NOT NULL,
\`title\` text DEFAULT '系统状态' NOT NULL,
\`description\` text DEFAULT '系统当前运行状态',
\`logo_url\` text DEFAULT '',
\`custom_css\` text DEFAULT '',
\`created_at\` text DEFAULT 'CURRENT_TIMESTAMP',
\`updated_at\` text DEFAULT 'CURRENT_TIMESTAMP',
FOREIGN KEY (\`user_id\`) REFERENCES \`users\`(\`id\`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`status_page_monitors\` (
\`config_id\` integer NOT NULL,
\`monitor_id\` integer NOT NULL,
PRIMARY KEY(\`config_id\`, \`monitor_id\`),
FOREIGN KEY (\`config_id\`) REFERENCES \`status_page_config\`(\`id\`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (\`monitor_id\`) REFERENCES \`monitors\`(\`id\`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS \`users\` (
\`id\` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
\`username\` text NOT NULL,
\`password\` text NOT NULL,
\`email\` text,
\`role\` text NOT NULL,
\`created_at\` text NOT NULL,
\`updated_at\` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS \`users_username_unique\` ON \`users\` (\`username\`);`
}
];
+1
View File
@@ -0,0 +1 @@
export * from "./seed";
+47
View File
@@ -0,0 +1,47 @@
import { MIGRATIONS } from "./generated-migrations";
import { Bindings } from "../models";
const tableExists = async (d1: Bindings["DB"], tableName: string): Promise<boolean | undefined> => {
const result = await d1.prepare("SELECT * FROM sqlite_master WHERE type='table' AND name=?").bind(tableName).run();
return result.success && result.results && result.results.length > 0;
}
// 执行所有迁移脚本
export async function runMigrations(d1: Bindings["DB"]): Promise<void> {
try {
console.log("开始执行数据库迁移...");
// 检查迁移记录表是否存在
const migrationsTableExists = await tableExists(d1, "migrations");
if (!migrationsTableExists) {
// 创建迁移记录表
await d1.prepare("CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, timestamp TEXT)").run();
console.log("迁移记录表创建成功");
}
// 执行迁移
for (const migration of MIGRATIONS) {
// 检查是否已执行过该迁移
const migrationResult = await d1.prepare("SELECT * FROM migrations WHERE name = ?").bind(migration.name).run();
if (migrationResult.results && migrationResult.results.length > 0) {
console.log(`迁移 ${migration.name} 已执行过,跳过`);
continue;
}
const result = await d1.prepare(migration.sql).run();
if (result.success) {
console.log(`迁移 ${migration.name} 成功`);
} else {
console.error(`迁移 ${migration.name} 失败`);
}
// 写入迁移记录
await d1.prepare("INSERT INTO migrations (name, timestamp) VALUES (?, ?)").bind(migration.name, new Date().toISOString()).run();
}
console.log("所有迁移已完成");
} catch (error) {
console.error("执行迁移脚本时出错:", error);
throw error;
}
}
+180
View File
@@ -0,0 +1,180 @@
import { int, sqliteTable, text, real, primaryKey } from "drizzle-orm/sqlite-core";
// 用户表
export const users = sqliteTable("users", {
id: int("id").primaryKey({ autoIncrement: true }),
username: text("username").notNull().unique(),
password: text("password").notNull(),
email: text("email"),
role: text("role").notNull(),
created_at: text("created_at").notNull(),
updated_at: text("updated_at").notNull()
});
// 监控表
export const monitors = sqliteTable("monitors", {
id: int("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
url: text("url").notNull(),
method: text("method").notNull(),
interval: int("interval").notNull(),
timeout: int("timeout").notNull(),
expected_status: int("expected_status").notNull(),
headers: text("headers").notNull(),
body: text("body"),
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()
});
// 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")
});
// 监控每日统计表
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()
});
// 客户端表
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),
status: text("status").default("inactive"),
created_at: text("created_at").notNull(),
updated_at: text("updated_at").notNull(),
hostname: text("hostname"),
ip_addresses: text("ip_addresses"),
os: text("os"),
version: text("version"),
keepalive: text("keepalive"),
});
// 客户端资源指标表
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")
});
// 状态页配置表
export const statusPageConfig = sqliteTable("status_page_config", {
id: int("id").primaryKey({ autoIncrement: true }),
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")
});
// 状态页监控项关联表
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(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(table.config_id, table.agent_id)
}));
// 通知渠道表
export const notificationChannels = sqliteTable("notification_channels", {
id: int("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
type: text("type").notNull(),
config: text("config").notNull(),
enabled: int("enabled").notNull().default(1),
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")
});
// 通知模板表
export const notificationTemplates = sqliteTable("notification_templates", {
id: int("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
type: text("type").notNull(),
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_at: text("created_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),
target_type: text("target_type").notNull().default("global"),
target_id: int("target_id"),
enabled: int("enabled").notNull().default(1),
on_down: int("on_down").notNull().default(1),
on_recovery: int("on_recovery").notNull().default(1),
on_offline: int("on_offline").notNull().default(1),
on_cpu_threshold: int("on_cpu_threshold").notNull().default(0),
cpu_threshold: int("cpu_threshold").notNull().default(90),
on_memory_threshold: int("on_memory_threshold").notNull().default(0),
memory_threshold: int("memory_threshold").notNull().default(85),
on_disk_threshold: int("on_disk_threshold").notNull().default(0),
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")
});
// 通知历史记录表
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),
status: text("status").notNull(),
content: text("content").notNull(),
error: text("error"),
sent_at: text("sent_at").default("CURRENT_TIMESTAMP")
});
+238
View File
@@ -0,0 +1,238 @@
import { Hono } from "hono";
import bcrypt from "bcryptjs";
import { runMigrations } from "./migration";
import {
users,
notificationTemplates,
notificationChannels,
notificationSettings,
statusPageConfig,
statusPageMonitors,
statusPageAgents,
monitors,
agents,
} from "./schema";
import { db } from "../config";
import { eq, desc } from "drizzle-orm";
import { Bindings } from "../models";
const seed = new Hono<{}>();
// 检查并初始化数据库
export async function checkAndInitializeDatabase(d1: Bindings["DB"]): Promise<{
initialized: boolean;
message: string;
}> {
try {
console.log("开始数据库初始化流程...");
// 1. 执行迁移
await runMigrations(d1);
// 2. 初始化基础数据(如果需要)
await createAdminUser();
await createNotificationTemplates();
await createNotificationChannelsAndSettings();
await createDefaultStatusPage();
return {
initialized: true,
message: "数据库初始化成功",
};
} catch (error) {
return {
initialized: false,
message: `数据库初始化失败: ${error}`,
};
}
}
// 创建管理员用户
export async function createAdminUser(): Promise<void> {
console.log("检查管理员用户...");
const adminUser = await db
.select()
.from(users)
.where(eq(users.username, "admin"));
// 如果不存在管理员用户,则创建一个
if (adminUser.length === 0) {
console.log("创建管理员用户...");
// 密码: admin123
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash("admin123", salt);
const now = new Date().toISOString();
await db.insert(users).values({
id: 1,
username: "admin",
password: hashedPassword,
email: "admin@mdzz.uk",
role: "admin",
created_at: now,
updated_at: now,
});
}
}
// 添加通知模板初始化函数
export async function createNotificationTemplates(): Promise<void> {
// 检查是否已有通知模板
const existingTemplates = await db.select().from(notificationTemplates).all();
if (existingTemplates.length === 0) {
console.log("添加默认通知模板...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
await db.insert(notificationTemplates).values({
id: 1,
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: 1,
created_by: userId,
created_at: now,
updated_at: now,
});
await db.insert(notificationTemplates).values({
id: 2,
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: 1,
created_by: userId,
created_at: now,
updated_at: now,
});
}
}
// 添加通知渠道和设置初始化函数
export async function createNotificationChannelsAndSettings(): Promise<void> {
// 检查是否已有通知渠道
const existingChannels = await db.select().from(notificationChannels).all();
if (existingChannels.length === 0) {
console.log("添加默认通知渠道...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
await db.insert(notificationChannels).values({
id: 1,
name: "默认Telegram通知渠道(https://t.me/xugou_group)",
type: "telegram",
config:
'{"botToken": "8163201319:AAGyY7FtdaRb6o8NCVXSbBUb6ofDK45cNJU", "chatId": "-1002608818360"}',
enabled: 1,
created_by: userId,
created_at: now,
updated_at: now,
});
}
// 检查是否已有通知设置
const existingSettings = await db.select().from(notificationSettings).all();
if (existingSettings.length === 0) {
console.log("添加默认通知设置...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
await db.insert(notificationSettings).values({
id: 1,
user_id: userId,
target_type: "global-monitor",
enabled: 0,
on_down: 1,
on_recovery: 1,
channels: "[1]", // channels (只有Telegram)
created_at: now,
updated_at: now,
});
await db.insert(notificationSettings).values({
id: 2,
user_id: userId,
target_type: "global-agent",
enabled: 0,
on_down: 1,
on_recovery: 1,
on_cpu_threshold: 1,
cpu_threshold: 80,
on_memory_threshold: 1,
memory_threshold: 80,
on_disk_threshold: 1,
disk_threshold: 90,
channels: "[1]", // channels (只有Telegram)
created_at: now,
updated_at: now,
});
}
}
// 创建默认状态页配置
export async function createDefaultStatusPage(): Promise<void> {
// 检查是否已有状态页配置
const existingConfig = await db.select().from(statusPageConfig).all();
if (existingConfig.length === 0) {
console.log("创建默认状态页配置...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
// 创建配置
await db.insert(statusPageConfig).values({
user_id: userId,
title: "系统状态",
description: "实时监控系统运行状态",
logo_url: "",
custom_css: "",
created_at: now,
updated_at: now,
});
// 获取配置ID
const config = await db
.select()
.from(statusPageConfig)
.orderBy(desc(statusPageConfig.id))
.limit(1);
const configId = config[0].id;
if (configId) {
// 关联所有监控项
const allmonitors = await db.select().from(monitors).all();
if (allmonitors) {
for (const monitor of allmonitors) {
await db.insert(statusPageMonitors).values({
config_id: configId,
monitor_id: monitor.id,
});
}
// 关联所有客户端
const allAgents = await db.select().from(agents).all();
if (allAgents) {
for (const agent of allAgents) {
await db.insert(statusPageAgents).values({
config_id: configId,
agent_id: agent.id,
});
}
}
}
}
}
}
export { seed };
+7 -14
View File
@@ -2,16 +2,12 @@ import { Hono, ExecutionContext } from "hono";
import { logger } from "hono/logger";
import { prettyJSON } from "hono/pretty-json";
import * as initDb from "./initialization";
import * as seed from "./db";
import { Bindings } from "./models/db";
import * as middlewares from "./middlewares";
import * as jobs from "./jobs";
import * as api from "./api";
// 添加全局变量声明
declare global {
var isInitialized: boolean;
}
import * as config from "./config";
// 创建Hono应用
const app = new Hono<{ Bindings: Bindings }>();
@@ -40,13 +36,6 @@ let dbInitialized = false;
export default {
// 处理 HTTP 请求
async fetch(request: Request, env: Bindings, ctx: ExecutionContext) {
// 静态初始化标志
if (!globalThis.isInitialized) {
console.log("第一次请求,初始化应用...");
// 设置初始化标志
globalThis.isInitialized = true;
}
// 如果是 OPTIONS 请求,直接处理
if (request.method === "OPTIONS") {
@@ -63,10 +52,14 @@ export default {
});
}
// 初始化 drizzle 实例
config.initDb(env);
// 如果数据库尚未初始化,则进行初始化检查
if (!dbInitialized) {
console.log("首次请求,检查数据库状态...");
const initResult = await initDb.checkAndInitializeDatabase(env);
const initResult = await seed.checkAndInitializeDatabase(env.DB);
dbInitialized = true;
console.log("数据库检查结果:", initResult.message);
}
-302
View File
@@ -1,302 +0,0 @@
// 初始化数据库脚本
// 用于在 Cloudflare Workers 环境中创建数据库表和初始数据
import { Hono } from "hono";
import bcrypt from "bcryptjs";
import { Bindings } from "../models/db";
const initDb = new Hono<{ Bindings: Bindings }>();
// 创建数据库表结构
export async function createTables(env: Bindings): Promise<void> {
console.log("创建用户表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, email TEXT, role TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL)"
);
console.log("创建监控表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS monitors (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, url TEXT NOT NULL, method TEXT NOT NULL, interval INTEGER NOT NULL, timeout INTEGER NOT NULL, expected_status INTEGER NOT NULL, headers TEXT NOT NULL, body TEXT, created_by INTEGER NOT NULL, active BOOLEAN NOT NULL, status TEXT DEFAULT 'pending', response_time INTEGER DEFAULT 0, last_checked TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY (created_by) REFERENCES users(id))"
);
console.log("创建24小时监控状态历史表(热表)...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS monitor_status_history_24h (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, status TEXT NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, response_time INTEGER, status_code INTEGER, error TEXT, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
);
console.log("创建监控每日统计表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS monitor_daily_stats (id INTEGER PRIMARY KEY AUTOINCREMENT, monitor_id INTEGER NOT NULL, date TEXT NOT NULL, total_checks INTEGER NOT NULL DEFAULT 0, up_checks INTEGER NOT NULL DEFAULT 0, down_checks INTEGER NOT NULL DEFAULT 0, avg_response_time INTEGER DEFAULT 0, min_response_time INTEGER DEFAULT 0, max_response_time INTEGER DEFAULT 0, availability REAL DEFAULT 0, created_at TEXT NOT NULL, FOREIGN KEY (monitor_id) REFERENCES monitors(id))"
);
console.log("创建客户端表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS agents (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, token TEXT NOT NULL UNIQUE, created_by INTEGER NOT NULL, status TEXT DEFAULT 'inactive', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, hostname TEXT, ip_addresses TEXT, os TEXT, version TEXT,keepalive TEXT, cpu_usage REAL, memory_total INTEGER, memory_used INTEGER, disk_total INTEGER, disk_used INTEGER, network_rx INTEGER, network_tx INTEGER, FOREIGN KEY (created_by) REFERENCES users(id))"
);
console.log("创建客户端资源指标表(24h热表)...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS agent_metrics_24h (id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id INTEGER NOT NULL, timestamp TEXT DEFAULT CURRENT_TIMESTAMP, cpu_usage REAL, cpu_cores INTEGER, cpu_model TEXT, memory_total INTEGER, memory_used INTEGER, memory_free INTEGER, memory_usage_rate REAL, load_1 REAL, load_5 REAL, load_15 REAL, disk_metrics TEXT, network_metrics TEXT, FOREIGN KEY (agent_id) REFERENCES agents(id))"
);
console.log("创建状态页配置表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS status_page_config (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, title TEXT NOT NULL DEFAULT '系统状态', description TEXT DEFAULT '系统当前运行状态', logo_url TEXT DEFAULT '', custom_css TEXT DEFAULT '', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id))"
);
console.log("创建状态页监控项关联表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS status_page_monitors (config_id INTEGER NOT NULL, monitor_id INTEGER NOT NULL, PRIMARY KEY (config_id, monitor_id), FOREIGN KEY (config_id) REFERENCES status_page_config(id) ON DELETE CASCADE, FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE)"
);
console.log("创建状态页客户端关联表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS status_page_agents (config_id INTEGER NOT NULL, agent_id INTEGER NOT NULL, PRIMARY KEY (config_id, agent_id), FOREIGN KEY (config_id) REFERENCES status_page_config(id) ON DELETE CASCADE, FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE)"
);
// 添加通知系统相关表
console.log("创建通知渠道表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS notification_channels (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, config TEXT NOT NULL, enabled BOOLEAN NOT NULL DEFAULT 1, created_by INTEGER NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id))"
);
console.log("创建通知模板表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS notification_templates (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type TEXT NOT NULL, subject TEXT NOT NULL, content TEXT NOT NULL, is_default BOOLEAN NOT NULL DEFAULT 0, created_by INTEGER NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES users(id))"
);
console.log("创建通知设置表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS notification_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, target_type TEXT NOT NULL DEFAULT 'global', target_id INTEGER DEFAULT NULL, enabled BOOLEAN NOT NULL DEFAULT 1, on_down BOOLEAN NOT NULL DEFAULT 1, on_recovery BOOLEAN NOT NULL DEFAULT 1, on_offline BOOLEAN NOT NULL DEFAULT 1, on_cpu_threshold BOOLEAN NOT NULL DEFAULT 0, cpu_threshold INTEGER NOT NULL DEFAULT 90, on_memory_threshold BOOLEAN NOT NULL DEFAULT 0, memory_threshold INTEGER NOT NULL DEFAULT 85, on_disk_threshold BOOLEAN NOT NULL DEFAULT 0, disk_threshold INTEGER NOT NULL DEFAULT 90, channels TEXT DEFAULT '[]', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), UNIQUE(user_id, target_type, target_id))"
);
console.log("创建通知历史记录表...");
await env.DB.exec(
"CREATE TABLE IF NOT EXISTS notification_history (id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, target_id INTEGER, channel_id INTEGER NOT NULL, template_id INTEGER NOT NULL, status TEXT NOT NULL, content TEXT NOT NULL, error TEXT, sent_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (channel_id) REFERENCES notification_channels(id), FOREIGN KEY (template_id) REFERENCES notification_templates(id))"
);
}
// 创建管理员用户
export async function createAdminUser(env: Bindings): Promise<void> {
console.log("检查管理员用户...");
const adminUser = await env.DB.prepare(
"SELECT id FROM users WHERE username = ?"
)
.bind("admin")
.first();
// 如果不存在管理员用户,则创建一个
if (!adminUser) {
console.log("创建管理员用户...");
// 密码: admin123
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash("admin123", salt);
const now = new Date().toISOString();
await env.DB.prepare(
`INSERT INTO users (username, password, email, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind("admin", hashedPassword, "admin@mdzz.uk", "admin", now, now)
.run();
}
}
// 添加通知模板初始化函数
export async function createNotificationTemplates(
env: Bindings
): Promise<void> {
// 检查是否已有通知模板
const existingTemplates = await env.DB.prepare(
"SELECT COUNT(*) as count FROM notification_templates"
).first<{ count: number }>();
if (existingTemplates.count === 0) {
console.log("添加默认通知模板...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
// 添加 Monitor 监控模板 (ID: 1)
await env.DB.prepare(
`INSERT INTO notification_templates (id, name, type, subject, content, is_default, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
1,
"Monitor监控模板",
"monitor",
"【${status}】${name} 监控状态变更",
"🔔 网站监控状态变更通知\n\n📊 服务: ${name}\n🔄 状态: ${status} (之前: ${previous_status})\n🕒 时间: ${time}\n\n🔗 地址: ${url}\n⏱️ 响应时间: ${response_time}\n📝 实际状态码: ${status_code}\n🎯 期望状态码: ${expected_status}\n\n❗ 错误信息: ${error}",
1, // is_default=1
userId,
now,
now
)
.run();
// 添加 Agent 客户端监控模板 (ID: 2)
await env.DB.prepare(
`INSERT INTO notification_templates (id, name, type, subject, content, is_default, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
2,
"Agent监控模板",
"agent",
"【${status}】${name} 客户端状态变更",
"🔔 客户端状态变更通知\n\n📊 主机: ${name}\n🔄 状态: ${status} (之前: ${previous_status})\n🕒 时间: ${time}\n\n🖥️ 主机信息:\n 主机名: ${hostname}\n IP地址: ${ip_addresses}\n 操作系统: ${os}\n\n❗ 错误信息: ${error}",
1, // is_default=1
userId,
now,
now
)
.run();
}
}
// 添加通知渠道和设置初始化函数
export async function createNotificationChannelsAndSettings(
env: Bindings
): Promise<void> {
// 检查是否已有通知渠道
const existingChannels = await env.DB.prepare(
"SELECT COUNT(*) as count FROM notification_channels"
).first<{ count: number }>();
if (existingChannels.count === 0) {
console.log("添加默认通知渠道...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
// 添加Telegram通知渠道 (ID: 1)
await env.DB.prepare(
`INSERT INTO notification_channels (id, name, type, config, enabled, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
1,
"默认Telegram通知渠道(https://t.me/xugou_group)",
"telegram",
'{"botToken": "8163201319:AAGyY7FtdaRb6o8NCVXSbBUb6ofDK45cNJU", "chatId": "-1002608818360"}',
1, // enabled
userId,
now,
now
)
.run();
}
// 检查是否已有通知设置
const existingSettings = await env.DB.prepare(
"SELECT COUNT(*) as count FROM notification_settings"
).first<{ count: number }>();
if (existingSettings.count === 0) {
console.log("添加默认通知设置...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
// 添加全局监控通知设置 (ID: 1)
await env.DB.prepare(
`INSERT INTO notification_settings (id, user_id, target_type, enabled, on_down, on_recovery, channels, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
1,
userId,
"global-monitor",
0, // enabled
1, // on_down
1, // on_recovery
"[1]", // channels (只有Telegram)
now,
now
)
.run();
// 添加全局客户端通知设置 (ID: 2)
await env.DB.prepare(
`INSERT INTO notification_settings (id, user_id, target_type, enabled, on_offline, on_recovery, on_cpu_threshold, cpu_threshold, on_memory_threshold, memory_threshold, on_disk_threshold, disk_threshold, channels, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
2,
userId,
"global-agent",
0, // enabled
1, // on_offline
1, // on_recovery
1, // on_cpu_threshold
80, // cpu_threshold
1, // on_memory_threshold
80, // memory_threshold
1, // on_disk_threshold
90, // disk_threshold
"[1]", // channels (只有Telegram)
now,
now
)
.run();
}
}
// 创建默认状态页配置
export async function createDefaultStatusPage(env: Bindings): Promise<void> {
// 检查是否已有状态页配置
const existingConfig = await env.DB.prepare(
"SELECT COUNT(*) as count FROM status_page_config"
).first<{ count: number }>();
if (existingConfig && existingConfig.count === 0) {
console.log("创建默认状态页配置...");
const now = new Date().toISOString();
const userId = 1; // 管理员用户ID
// 创建配置
const result = await env.DB.prepare(
`INSERT INTO status_page_config (user_id, title, description, logo_url, custom_css, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`
)
.bind(userId, "系统状态", "实时监控系统运行状态", "", "", now, now)
.run();
// 获取配置ID
const configId = await env.DB.prepare(
"SELECT last_insert_rowid() as id"
).first<{ id: number }>();
if (configId && configId.id) {
// 关联所有监控项
const monitors = await env.DB.prepare("SELECT id FROM monitors").all<{
id: number;
}>();
if (monitors.results) {
for (const monitor of monitors.results) {
await env.DB.prepare(
"INSERT INTO status_page_monitors (config_id, monitor_id) VALUES (?, ?)"
)
.bind(configId.id, monitor.id)
.run();
}
}
// 关联所有客户端
const agents = await env.DB.prepare("SELECT id FROM agents").all<{
id: number;
}>();
if (agents.results) {
for (const agent of agents.results) {
await env.DB.prepare(
"INSERT INTO status_page_agents (config_id, agent_id) VALUES (?, ?)"
)
.bind(configId.id, agent.id)
.run();
}
}
}
}
}
export { initDb };
-2
View File
@@ -1,2 +0,0 @@
export * from "./database";
export * from "./initCheck";
-104
View File
@@ -1,104 +0,0 @@
/**
* 数据库初始化检查
* 用于应用启动时检测数据库是否为空,如果为空则初始化
*/
import { Bindings } from "../models/db";
import {
createTables,
createAdminUser,
createNotificationTemplates,
createNotificationChannelsAndSettings,
createDefaultStatusPage,
} from "./database";
import { runMigrations } from "../migrations/migration";
// 检查表是否存在
async function tableExists(env: Bindings, tableName: string): Promise<boolean> {
try {
const result = await env.DB.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
)
.bind(tableName)
.first<{ name: string }>();
return !!result;
} catch (error) {
console.error(`检查表 ${tableName} 是否存在时出错:`, error);
return false;
}
}
// 检查并初始化数据库
export async function checkAndInitializeDatabase(
env: Bindings
): Promise<{ initialized: boolean; message: string }> {
try {
console.log("检查数据库是否需要初始化...");
// 要检查的表列表
const tablesToCheck = [
"users",
"monitors",
"monitor_status_history_24h",
"monitor_daily_stats",
"agents",
"agent_metrics_24h",
"status_page_config",
"status_page_monitors",
"status_page_agents",
"notification_channels",
"notification_templates",
"notification_settings",
"notification_history",
];
// 检查每个表是否存在
let missingTables: string[] = [];
for (const table of tablesToCheck) {
// 检查表是否存在
const exists = await tableExists(env, table);
if (!exists) {
console.log(`表 ${table} 不存在`);
missingTables.push(table);
}
}
let initialized = false;
// 如果有表不存在那就创建
if (missingTables.length > 0) {
console.log("开始初始化数据库...");
console.log(
"缺失的表:",
missingTables.length > 0 ? missingTables.join(", ") : "无"
);
// 创建表结构(只创建不存在的表)
console.log("创建缺失的表结构...");
await createTables(env);
}
await createAdminUser(env);
await createNotificationTemplates(env);
await createNotificationChannelsAndSettings(env);
await createDefaultStatusPage(env);
initialized = true;
// 执行迁移
await runMigrations(env);
return {
initialized,
message: initialized
? "数据库初始化成功"
: "数据库已经初始化,不需要重新初始化",
};
} catch (error) {
return {
initialized: false,
message: `数据库初始化失败: ${error}`,
};
}
}
+26 -26
View File
@@ -8,6 +8,9 @@ import {
import { shouldSendNotification, sendNotification } from "../services";
import { Hono } from "hono";
import { Bindings } from "../models/db";
import { db } from "../config";
import { eq } from "drizzle-orm";
import { notificationSettings } from "../db/schema";
const agentTask = new Hono<{ Bindings: Bindings }>();
@@ -41,7 +44,7 @@ export const checkAgentsStatus = async (c: any) => {
const timeDiff = now.getTime() - lastUpdateTime.getTime();
// 如果超过5个监控周期没有更新状态,将客户端状态设置为inactive
if (timeDiff > (parseInt(agent.keepalive || "60") * 5 * 1000)) {
if (timeDiff > parseInt(agent.keepalive || "60") * 5 * 1000) {
console.log(
`定时任务: 客户端 ${agent.name} (ID: ${agent.id}) 超过5个监控周期未更新状态,设置为离线`
);
@@ -72,7 +75,6 @@ async function handleAgentOfflineNotification(
try {
// 检查是否需要发送通知
const notificationCheck = await shouldSendNotification(
env.DB,
"agent",
agentId,
"online", // 上一个状态
@@ -92,8 +94,8 @@ async function handleAgentOfflineNotification(
console.log(`客户端 ${agentName} (ID: ${agentId}) 已离线,正在发送通知...`);
// 获取客户端完整信息
const agentData = await getAgentById(env.DB, agentId);
const agent = await getAgentById(agentId);
const agentData = agent[0];
if (!agentData) {
console.error(`找不到客户端数据 (ID: ${agentId})`);
return;
@@ -120,7 +122,6 @@ async function handleAgentOfflineNotification(
// 发送通知
const notificationResult = await sendNotification(
env.DB,
"agent",
agentId,
variables,
@@ -145,18 +146,17 @@ async function handleAgentOfflineNotification(
* 此函数可以单独调用,也可以在客户端上报数据时触发
*/
export async function handleAgentThresholdNotification(
env: any,
agentId: number,
metricType: string,
value: number
) {
try {
// 获取客户端配置
const agent = await getAgentById(env.DB, agentId);
const agent = await getAgentById(agentId);
if (!agent) {
console.error(`找不到客户端 (ID: ${agentId})`);
return;
throw new Error(`找不到客户端 (ID: ${agentId})`);
}
// 根据具体的指标类型
@@ -165,19 +165,22 @@ export async function handleAgentThresholdNotification(
let shouldSend = false;
// 查询特定设置
const settings = await env.DB.prepare(
`SELECT * FROM notification_settings WHERE target_type = ? AND target_id = ? AND enabled = 1`
)
.bind("agent", agentId)
.first();
const settings = await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.target_type, "agent"))
.where(eq(notificationSettings.target_id, agentId))
.where(eq(notificationSettings.enabled, 1))
.limit(1);
// 如果没有特定设置,查询全局设置
const globalSettings = !settings
? await env.DB.prepare(
`SELECT * FROM notification_settings WHERE target_type = ? AND enabled = 1`
)
.bind("global-agent")
.first()
? await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.target_type, "global-agent"))
.where(eq(notificationSettings.enabled, 1))
.limit(1)
: null;
// 使用特定设置或全局设置
@@ -260,7 +263,6 @@ export async function handleAgentThresholdNotification(
// 发送通知
const notificationResult = await sendNotification(
env.DB,
"agent",
agentId,
variables,
@@ -284,7 +286,6 @@ export async function handleAgentThresholdNotification(
// 在 Cloudflare Workers 中设置定时触发器
export default {
async scheduled(event: any, env: any, ctx: any) {
const c = { env };
// 默认执行监控检查任务
@@ -292,18 +293,17 @@ export default {
// 获取24小时前的时间
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const hour = new Date().getUTCHours();
const minute = new Date().getUTCMinutes()
const minute = new Date().getUTCMinutes();
// 每隔6小时清理一次 metrics 24h 表数据
if ((hour%6) === 0 && minute === 5){
if (hour % 6 === 0 && minute === 5) {
console.log("定时任务: 正在清理 metrics 24h 表数据...");
await env.DB.prepare(
"DELETE FROM agent_metrics_24h WHERE timestamp < ?"
).bind(yesterday).run();
await env.DB.prepare("DELETE FROM agent_metrics_24h WHERE timestamp < ?")
.bind(yesterday)
.run();
}
return result;
},
fetch: agentTask.fetch,
};
+38 -65
View File
@@ -3,6 +3,9 @@ import { Bindings } from "../models/db";
import { Monitor } from "../models/monitor";
import { getMonitorsToCheck, checkMonitor } from "../services";
import { shouldSendNotification, sendNotification } from "../services";
import { db } from "../config";
import { monitorDailyStats, monitorStatusHistory24h,monitors } from "../db/schema";
import { and, gte, lte } from "drizzle-orm";
const monitorTask = new Hono<{ Bindings: Bindings }>();
@@ -14,7 +17,7 @@ async function checkMonitors(c: any) {
const now = new Date();
// 查询需要检查的监控
const monitors = await getMonitorsToCheck(c.env.DB);
const monitors = await getMonitorsToCheck();
console.log(`找到 ${monitors?.results?.length || 0} 个需要检查的监控`);
@@ -27,7 +30,7 @@ async function checkMonitors(c: any) {
monitors.results.map(async (monitorItem: any) => {
// 确保正确的类型转换
const monitor = monitorItem as Monitor;
const checkResult = await checkMonitor(c.env.DB, monitor);
const checkResult = await checkMonitor(monitor);
// 处理通知
await handleMonitorNotification(c, monitor, checkResult);
@@ -74,7 +77,6 @@ async function handleMonitorNotification(
// 检查是否需要发送通知
console.log(`检查通知设置...`);
const notificationCheck = await shouldSendNotification(
c.env.DB,
"monitor",
monitor.id,
checkResult.previous_status,
@@ -127,7 +129,6 @@ async function handleMonitorNotification(
// 发送通知
console.log(`开始发送通知...`);
const notificationResult = await sendNotification(
c.env.DB,
"monitor",
monitor.id,
variables,
@@ -167,41 +168,34 @@ async function generateDailyStats(c: any) {
const endTime = `${dateStr}T23:59:59.999Z`;
// 一次性获取所有监控
const monitorsResult = await c.env.DB.prepare(
"SELECT id, name FROM monitors"
).all();
const monitorsResult = await db.select().from(monitors);
if (!monitorsResult.results || monitorsResult.results.length === 0) {
console.log("没有找到监控");
return { success: true, message: "没有监控", processed: 0 };
}
const monitors = monitorsResult.results;
console.log(`找到 ${monitors.length} 个监控`);
const allMonitors = monitorsResult.results as Monitor[];
console.log(`找到 ${allMonitors.length} 个监控`);
// 创建监控ID列表
const monitorIds = monitors.map((m: any) => m.id);
const monitorIds = allMonitors.map((m: any) => m.id);
// 从24小时热表获取监控历史记录
console.log(
`从24小时热表查询所有监控在 ${startTime} 至 ${endTime} 的历史记录`
);
const historyResult = await c.env.DB.prepare(
`
SELECT
monitor_id,
status,
response_time
FROM
monitor_status_history_24h
WHERE
timestamp >= ? AND
timestamp <= ?
`
)
.bind(startTime, endTime)
.all();
const historyResult = await db
.select()
.from(monitorStatusHistory24h)
.where(
and(
gte(monitorStatusHistory24h.timestamp, startTime),
lte(monitorStatusHistory24h.timestamp, endTime)
)
);
if (!historyResult.results || historyResult.results.length === 0) {
console.log(`在 ${dateStr} 没有找到任何监控历史记录`);
@@ -280,7 +274,7 @@ async function generateDailyStats(c: any) {
for (const [monitorId, stats] of statsMap.entries()) {
if (stats.totalChecks === 0) continue;
const monitor = monitors.find((m: any) => m.id === monitorId);
const monitor = allMonitors.find((m: any) => m.id === monitorId);
const monitorName = monitor ? monitor.name : `ID: ${monitorId}`;
try {
@@ -292,35 +286,18 @@ async function generateDailyStats(c: any) {
}, 可用率=${stats.availability.toFixed(2)}%`
);
await c.env.DB.prepare(
`
INSERT INTO monitor_daily_stats (
monitor_id,
date,
total_checks,
up_checks,
down_checks,
avg_response_time,
min_response_time,
max_response_time,
availability,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
)
.bind(
monitorId,
dateStr,
stats.totalChecks,
stats.upChecks,
stats.downChecks,
stats.avgResponseTime,
stats.minResponseTime,
stats.maxResponseTime,
stats.availability,
now
)
.run();
await db.insert(monitorDailyStats).values({
monitor_id: monitorId,
date: dateStr,
total_checks: stats.totalChecks,
up_checks: stats.upChecks,
down_checks: stats.downChecks,
avg_response_time: stats.avgResponseTime,
min_response_time: stats.minResponseTime,
max_response_time: stats.maxResponseTime,
availability: stats.availability,
created_at: now,
});
processed++;
console.log(`成功更新监控 ID ${monitorId} 的每日统计数据`);
@@ -333,16 +310,12 @@ async function generateDailyStats(c: any) {
// 从 24h 表中删除已处理的数据
console.log(`开始从24小时热表删除已处理的数据`);
const deleteResult = await c.env.DB.prepare(
`
DELETE FROM monitor_status_history_24h
WHERE
timestamp >=? AND
timestamp <=?
`
)
.bind(startTime, endTime)
.run();
await db.delete(monitorStatusHistory24h).where(
and(
gte(monitorStatusHistory24h.timestamp, startTime),
lte(monitorStatusHistory24h.timestamp, endTime)
)
);
console.log(`从24小时热表删除已处理的数据完成`);
return {
@@ -1,50 +0,0 @@
/**
* 迁移脚本: 从0.0.3版本迁移到0.0.4版本
* 主要变更: 确保 agents 表中有 ip_addresses 字段
*/
import { Bindings } from "../models/db";
import { tableExists } from "./migrate-0.0.4-to-0.0.5";
/**
* 迁移脚本: 从0.0.3版本迁移到0.0.4版本
* 主要变更: 确保 agents 表中有 ip_addresses 字段
*/
export async function migrateFrom003To004(
env: Bindings
): Promise<{ success: boolean; message: string }> {
try {
console.log("开始从v0.0.3迁移到v0.0.4...");
// 执行迁移 - 添加 ip_addresses 字段
console.log("添加 ip_addresses 字段到 agents 表...");
// 检查 ip_addresses 字段是否已存在
if (await tableExists(env, "agents")) {
// 尝试查询 ip_addresses 字段的值,如果字段不存在会抛出错误
await env.DB.prepare(`SELECT ip_addresses FROM agents LIMIT 1`).first();
console.log("ip_addresses 字段已存在,跳过添加");
return {
success: true,
message: "ip_addresses 字段已存在,跳过添加",
};
}
await env.DB.exec(`ALTER TABLE agents ADD COLUMN ip_addresses TEXT`);
console.log("迁移完成: ip_addresses 字段已添加到 agents 表");
return {
success: true,
message: "从v0.0.3到v0.0.4的迁移成功完成",
};
} catch (error) {
console.error("迁移错误:", error);
return {
success: false,
message: `迁移失败: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
@@ -1,67 +0,0 @@
/**
* 迁移脚本: 从0.0.4版本迁移到0.0.5版本
* 主要变更: 修正 notification_templates 表中的 type 字段值
*/
import { Bindings } from "../models/db";
/**
* 检查表是否存在
*/
export async function tableExists(env: Bindings, table: string): Promise<boolean> {
try {
const result = await env.DB.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
)
.bind(table)
.first<{ name: string }>();
return !!result;
} catch (error) {
console.error(`检查表 ${table} 是否存在时出错:`, error);
return false;
}
}
export async function migrateFrom004To005(
env: Bindings
): Promise<{ success: boolean; message: string }> {
try {
console.log("开始从v0.0.4迁移到v0.0.5...");
// 检查 notification_templates 表是否存在
const hasTemplatesTable = await tableExists(env, "notification_templates");
if (!hasTemplatesTable) {
console.log("notification_templates 表不存在,跳过迁移");
return {
success: true,
message: "notification_templates 表不存在,跳过迁移",
};
}
// 再执行两个直接更新语句,确保覆盖所有情况
await env.DB.exec(
"UPDATE notification_templates SET type = 'agent' WHERE name LIKE '%Agent%' OR name LIKE '%agent%'"
);
await env.DB.exec(
"UPDATE notification_templates SET type = 'monitor' WHERE name LIKE '%Monitor%' OR name LIKE '%monitor%'"
);
console.log(`迁移完成`);
return {
success: true,
message: `从v0.0.4到v0.0.5的迁移成功完成`,
};
} catch (error) {
console.error("迁移错误:", error);
return {
success: false,
message: `迁移失败: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
@@ -1,62 +0,0 @@
/**
* 迁移脚本: 从0.0.4版本迁移到0.0.5版本
* 主要变更: 修正 notification_templates 表中的 type 字段值
*/
import { Bindings } from "../models/db";
/**
* 检查表是否存在
*/
export async function tableExists(
env: Bindings,
table: string
): Promise<boolean> {
try {
const result = await env.DB.prepare(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`
)
.bind(table)
.first<{ name: string }>();
return !!result;
} catch (error) {
console.error(`检查表 ${table} 是否存在时出错:`, error);
return false;
}
}
export async function migrateFrom007To008(
env: Bindings
): Promise<{ success: boolean; message: string }> {
try {
console.log("开始从v0.0.4迁移到v0.0.5...");
// 检查 agents 表是否存在
const hasTemplatesTable = await tableExists(env, "agents");
if (!hasTemplatesTable) {
console.log("agents 表不存在,跳过迁移");
return {
success: true,
message: "agents 表不存在,跳过迁移",
};
}
await env.DB.exec("ALTER TABLE agents ADD COLUMN keepalive TEXT");
console.log(`迁移完成`);
return {
success: true,
message: `从v0.0.7到v0.0.8的迁移成功完成`,
};
} catch (error) {
console.error("迁移错误:", error);
return {
success: false,
message: `迁移失败: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}
-28
View File
@@ -1,28 +0,0 @@
import { Bindings } from "../models/db";
import { migrateFrom003To004 } from "./migrate-0.0.3-to-0.0.4";
import { migrateFrom004To005 } from "./migrate-0.0.4-to-0.0.5";
import { migrateFrom007To008 } from "./migrate-0.0.7-to-0.0.8";
// 执行所有迁移脚本
export async function runMigrations(env: Bindings): Promise<void> {
try {
console.log("开始执行数据库迁移...");
// 从v0.0.3迁移到v0.0.4
const migration1Result = await migrateFrom003To004(env);
console.log(`迁移 0.0.3 -> 0.0.4: ${migration1Result.message}`);
// 从v0.0.4迁移到v0.0.5
const migration2Result = await migrateFrom004To005(env);
console.log(`迁移 0.0.4 -> 0.0.5: ${migration2Result.message}`);
// 从 v0.0.7 迁移到 v0.0.8
const migration3Result = await migrateFrom007To008(env);
console.log(`迁移 0.0.7 -> 0.0.8: ${migration3Result.message}`)
console.log("所有迁移已完成");
} catch (error) {
console.error("执行迁移脚本时出错:", error);
throw error;
}
}
+28 -43
View File
@@ -1,5 +1,21 @@
// 客户端类型定义
export interface Agent {
id: number;
name: string;
token: string;
created_by: number;
status: string | null;
created_at: string;
updated_at: string;
hostname: string | null;
keepalive: string | null;
ip_addresses: string | null; // 存储多个IP地址的JSON字符串
os: string | null;
version: string | null;
}
// 客户端类型定义
export interface AgentWithMetrics {
id: number;
name: string;
token: string;
@@ -16,49 +32,18 @@ export interface Agent {
}
export interface Metrics {
id: number;
agent_id: number;
timestamp: string;
cpu: CPUInfo;
memory: MemoryInfo;
disks: DiskInfo[];
networks: NetworkInfo[];
load: LoadInfo;
cpu_usage: number;
cpu_cores: number;
cpu_model: string;
memory_total: number;
memory_used: number;
memory_free: number;
memory_usage_rate: number;
load_1: number;
load_5: number;
load_15: number;
disk_metrics: string;
network_metrics: string;
}
export interface CPUInfo {
usage: number;
cores: number;
model_name: string;
}
export interface MemoryInfo{
total: number;
used: number;
free: number;
usage_rate: number;
}
export interface DiskInfo{
device: string;
mount_point: string;
total: number;
used: number;
free: number;
usage_rate: number;
fs_type: string;
}
export interface NetworkInfo{
interface: string;
bytes_sent: number;
bytes_recv: number;
packets_sent: number;
packets_recv: number;
}
export interface LoadInfo{
load1: number;
load5: number;
load15: number;
}
-6
View File
@@ -14,12 +14,6 @@ export interface D1PreparedStatement {
raw<T = unknown>(): Promise<T[]>;
}
// 定义操作结果的元数据类型
export interface DbResultMeta {
changes?: number;
[key: string]: any;
}
// 扩展D1Result meta属性的类型
export interface D1Meta {
last_row_id?: number;
+1 -1
View File
@@ -28,7 +28,7 @@ export interface NotificationSettings {
id: number;
user_id: number;
target_type: string; // global-monitor, global-agent, monitor, agent
target_id: number | null; // 当target_type为monitor或agent时有效,存储monitor_id或agent_id
target_id: number | 0; // 当target_type为monitor或agent时有效,存储monitor_id或agent_id
enabled: boolean;
on_down: boolean; // 适用于monitor类型
+65 -150
View File
@@ -1,58 +1,44 @@
import { Bindings } from "../models/db";
import { Agent, Metrics } from "../models/agent";
import { agents, agentMetrics24h } from "../db/schema";
import { db } from "../config";
import { desc, eq, inArray } from "drizzle-orm";
/**
* 客户端相关的数据库操作
*/
// 获取所有客户端
export async function getAllAgents(db: Bindings["DB"]) {
return await db
.prepare("SELECT * FROM agents ORDER BY created_at DESC")
.all<Agent>();
export async function getAllAgents() {
return await db.select().from(agents).orderBy(desc(agents.created_at));
}
// 批量获取客户端详情
export async function getAgentsByIds(db: Bindings["DB"], agentIds: number[]) {
export async function getAgentsByIds(agentIds: number[]) {
if (agentIds.length === 0) {
return { results: [] };
}
const placeholders = agentIds.map(() => "?").join(",");
return await db
.prepare(`SELECT * FROM agents WHERE id IN (${placeholders})`)
.bind(...agentIds)
.all<Agent>();
return await db.select().from(agents).where(inArray(agents.id, agentIds));
}
// 批量获取客户端指标
export async function getAgentMetricsByIds(
db: Bindings["DB"],
agentIds: number[]
) {
export async function getAgentMetricsByIds(agentIds: number[]) {
if (agentIds.length === 0) {
return { results: [] };
}
const placeholders = agentIds.map(() => "?").join(",");
return await db
.prepare(
`SELECT * FROM agent_metrics_24h WHERE agent_id IN (${placeholders})`
)
.bind(...agentIds)
.all<Metrics>();
.select()
.from(agentMetrics24h)
.where(inArray(agentMetrics24h.agent_id, agentIds));
}
// 获取单个客户端详情
export async function getAgentById(db: Bindings["DB"], id: number) {
return await db
.prepare("SELECT * FROM agents WHERE id = ?")
.bind(id)
.first<Agent>();
export async function getAgentById(id: number) {
const agent = await db.select().from(agents).where(eq(agents.id, id)).limit(1);
return agent[0];
}
// 创建新客户端
export async function createAgent(
db: Bindings["DB"],
name: string,
token: string,
createdBy: number,
@@ -68,113 +54,47 @@ export async function createAgent(
// 将 ipAddresses 数组转换为 JSON 字符串
const ipAddressesJson = ipAddresses ? JSON.stringify(ipAddresses) : null;
const result = await db
.prepare(
`INSERT INTO agents
(name, token, created_by, status, created_at, updated_at, hostname, ip_addresses, os, version, keepalive)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)`
)
.bind(
name,
token,
createdBy,
status,
now,
now,
hostname,
ipAddressesJson,
os,
version,
keepalive
)
.run();
const result = await db.insert(agents).values({
name,
token,
created_by: createdBy,
status,
created_at: now,
updated_at: now,
hostname,
ip_addresses: ipAddressesJson,
os,
version,
keepalive,
}).returning();
if (!result.success) {
if (!result) {
throw new Error("创建客户端失败");
}
// 获取新创建的客户端
return await db
.prepare("SELECT * FROM agents WHERE rowid = last_insert_rowid()")
.first<Agent>();
return result[0];
}
// 更新客户端信息
export async function updateAgent(
db: Bindings["DB"],
id: number,
fields: {
name?: string;
hostname?: string;
ip_addresses?: string[];
os?: string;
version?: string;
status?: string;
keepalive?: string;
}
agent: Agent
) {
const fieldsToUpdate = [];
const values = [];
// 添加需要更新的字段
for (const [key, value] of Object.entries(fields)) {
if (value !== undefined) {
// 特殊处理 ip_addresses 数组,转换为 JSON 字符串
if (key === "ip_addresses" && Array.isArray(value)) {
fieldsToUpdate.push(`${key} = ?`);
values.push(JSON.stringify(value));
} else {
fieldsToUpdate.push(`${key} = ?`);
values.push(value);
}
}
}
// 添加更新时间
fieldsToUpdate.push("updated_at = ?");
values.push(new Date().toISOString());
// 添加ID作为条件
values.push(id);
// 如果没有要更新的字段,直接返回
if (fieldsToUpdate.length === 1) {
// 只有updated_at
return { success: true, message: "没有更新任何字段" };
}
// 执行更新
const updateSql = `
UPDATE agents
SET ${fieldsToUpdate.join(", ")}
WHERE id = ?
`;
const result = await db
.prepare(updateSql)
.bind(...values)
.run();
if (!result.success) {
agent.updated_at = new Date().toISOString();
try {
const updatedAgent = await db.update(agents).set(agent).where(eq(agents.id, agent.id)).returning();
return updatedAgent[0];
} catch (error) {
console.error("更新客户端失败:", error);
throw new Error("更新客户端失败");
}
// 获取更新后的客户端
return result.success;
}
// 删除客户端
export async function deleteAgent(db: Bindings["DB"], id: number) {
export async function deleteAgent(id: number) {
// 先删除关联的指标数据
await db
.prepare("DELETE FROM agent_metrics_24h WHERE agent_id = ?")
.bind(id)
.run();
await db.delete(agentMetrics24h).where(eq(agentMetrics24h.agent_id, id));
const result = await db
.prepare("DELETE FROM agents WHERE id = ?")
.bind(id)
.run();
const result = await db.delete(agents).where(eq(agents.id, id));
if (!result.success) {
throw new Error("删除客户端失败");
@@ -184,54 +104,49 @@ export async function deleteAgent(db: Bindings["DB"], id: number) {
}
// 通过令牌获取客户端
export async function getAgentByToken(db: Bindings["DB"], token: string) {
return await db
.prepare("SELECT * FROM agents WHERE token = ?")
.bind(token)
.first<Agent>();
export async function getAgentByToken(token: string) {
const agent = await db.select().from(agents).where(eq(agents.token, token));
return agent[0];
}
// 获取活跃状态的客户端
export async function getActiveAgents(db: Bindings["DB"]) {
export async function getActiveAgents() {
return await db
.prepare('SELECT id, name, updated_at, keepalive FROM agents WHERE status = "active"')
.all();
.select("id", "name", "updated_at", "keepalive")
.from(agents)
.where(eq(agents.status, "active"));
}
// 设置客户端为离线状态
export async function setAgentInactive(db: Bindings["DB"], id: number) {
export async function setAgentInactive(id: number) {
const now = new Date().toISOString();
return await db
.prepare(
'UPDATE agents SET status = "inactive", updated_at = ? WHERE id = ?'
)
.bind(now, id)
.run();
.update(agents)
.set({ status: "inactive", updated_at: now })
.where(eq(agents.id, id));
}
// 插入客户端资源指标
export async function insertAgentMetrics(db: Bindings["DB"], metrics: any) {
return await db
.prepare(
"INSERT INTO agent_metrics_24h (agent_id, timestamp, cpu_usage, cpu_cores, cpu_model, memory_total, memory_used, memory_free, memory_usage_rate, load_1, load_5, load_15, disk_metrics, network_metrics) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.bind(...metrics)
.run();
export async function insertAgentMetrics(metrics: Metrics[]) {
return await db.batch(metrics.map(metric => db.insert(agentMetrics24h).values(metric)));
}
// 获取指定客户端资源指标
export async function getAgentMetrics(db: Bindings["DB"], agentId: number) {
export async function getAgentMetrics(agentId: number) {
return await db
.prepare("SELECT * FROM agent_metrics_24h WHERE agent_id =?")
.bind(agentId)
.all();
.select()
.from(agentMetrics24h)
.where(eq(agentMetrics24h.agent_id, agentId));
}
// 获取指定客户端的最新指标
export async function getLatestAgentMetrics(db: Bindings["DB"], agentId: number) {
return await db
.prepare("SELECT * FROM agent_metrics_24h WHERE agent_id =? ORDER BY timestamp DESC LIMIT 1")
.bind(agentId)
.all();
export async function getLatestAgentMetrics(agentId: number) {
const metrics = await db
.select()
.from(agentMetrics24h)
.where(eq(agentMetrics24h.agent_id, agentId))
.orderBy(desc(agentMetrics24h.timestamp))
.limit(1);
return metrics[0];
}
+108 -153
View File
@@ -1,30 +1,35 @@
import { Monitor } from "../models";
import { db } from "../config";
import {
Bindings,
Monitor,
MonitorStatusHistory,
DbResultMeta,
MonitorDailyStats,
} from "../models";
monitors,
monitorStatusHistory24h,
monitorDailyStats,
} from "../db/schema";
import { eq, desc, asc, and, or, isNull, gte } from "drizzle-orm";
/**
* 监控相关的数据库操作
*/
// 获取需要检查的监控列表
export async function getMonitorsToCheck(db: Bindings["DB"]) {
export async function getMonitorsToCheck() {
// 这里的查询写的是有问题的,需要获取的是当前时间大于last_checked + interval的监控
// 但是interval是字符串,需要转换为数字
// 并且last_checked是字符串,需要转换为日期
const result = await db
.prepare(
`
SELECT * FROM monitors
WHERE active = true
AND (last_checked IS NULL OR datetime('now') > datetime(last_checked, '+' || interval || ' seconds'))
`
)
.all<Monitor>();
.select()
.from(monitors)
.where(
and(
eq(monitors.active, 1),
or(isNull(monitors.last_checked), gte(monitors.interval, 0))
)
);
// 解析所有监控的 headers 字段
if (result.results) {
result.results.forEach((monitor) => {
result.results.forEach((monitor: Monitor) => {
if (typeof monitor.headers === "string") {
try {
monitor.headers = JSON.parse(monitor.headers);
@@ -38,11 +43,8 @@ export async function getMonitorsToCheck(db: Bindings["DB"]) {
}
// 获取单个监控详情
export async function getMonitorById(db: Bindings["DB"], id: number) {
const monitor = await db
.prepare("SELECT * FROM monitors WHERE id = ?")
.bind(id)
.first<Monitor>();
export async function getMonitorById(id: number) {
const monitor = await db.select().from(monitors).where(eq(monitors.id, id));
// 解析 headers 字段
if (monitor && typeof monitor.headers === "string") {
@@ -52,18 +54,19 @@ export async function getMonitorById(db: Bindings["DB"], id: number) {
monitor.headers = {};
}
}
return monitor;
return monitor[0];
}
// 获取所有监控
export async function getAllMonitors(db: Bindings["DB"]) {
export async function getAllMonitors() {
const result = await db
.prepare("SELECT * FROM monitors ORDER BY created_at DESC")
.all<Monitor>();
.select()
.from(monitors)
.orderBy(desc(monitors.created_at));
// 解析所有监控的 headers 字段
if (result.results) {
result.results.forEach((monitor) => {
if (result) {
result.forEach((monitor: Monitor) => {
if (typeof monitor.headers === "string") {
try {
monitor.headers = JSON.parse(monitor.headers);
@@ -74,40 +77,27 @@ export async function getAllMonitors(db: Bindings["DB"]) {
});
}
return {
success: true,
monitors: result.results || [],
status: 200,
};
return result;
}
// 获取单个监控状态历史 24小时内
export async function getMonitorStatusHistoryIn24h(
db: Bindings["DB"],
monitorId: number
) {
export async function getMonitorStatusHistoryIn24h(monitorId: number) {
return await db
.prepare(
`SELECT * FROM monitor_status_history_24h
WHERE monitor_id = ?
ORDER BY timestamp ASC`
)
.bind(monitorId)
.all<MonitorStatusHistory>();
.select()
.from(monitorStatusHistory24h)
.where(eq(monitorStatusHistory24h.monitor_id, monitorId))
.orderBy(asc(monitorStatusHistory24h.timestamp));
}
// 获取所有监控状态历史 24小时内
export async function getAllMonitorStatusHistoryIn24h(db: Bindings["DB"]) {
export async function getAllMonitorStatusHistoryIn24h() {
return await db
.prepare(
`SELECT * FROM monitor_status_history_24h
ORDER BY timestamp ASC`
)
.all<MonitorStatusHistory>();
.select()
.from(monitorStatusHistory24h)
.orderBy(asc(monitorStatusHistory24h.timestamp));
}
// 记录监控状态历史到热表
export async function insertMonitorStatusHistory(
db: Bindings["DB"],
monitorId: number,
status: string,
response_time: number,
@@ -117,18 +107,18 @@ export async function insertMonitorStatusHistory(
// 使用ISO格式的时间戳
const now = new Date().toISOString();
return await db
.prepare(
`INSERT INTO monitor_status_history_24h (monitor_id, status, timestamp, response_time, status_code, error)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(monitorId, status, now, response_time, status_code, error)
.run<MonitorStatusHistory>();
return await db.insert(monitorStatusHistory24h).values({
monitor_id: monitorId,
status: status,
timestamp: now,
response_time: response_time,
status_code: status_code,
error: error,
});
}
// 更新监控状态
export async function updateMonitorStatus(
db: Bindings["DB"],
monitorId: number,
status: string,
responseTime: number
@@ -137,20 +127,17 @@ export async function updateMonitorStatus(
const now = new Date().toISOString();
return await db
.prepare(
`UPDATE monitors
SET status = ?,
last_checked = ?,
response_time = ?
WHERE id = ?`
)
.bind(status, now, responseTime, monitorId)
.run();
.update(monitors)
.set({
status: status,
last_checked: now,
response_time: responseTime,
})
.where(eq(monitors.id, monitorId));
}
// 创建新监控
export async function createMonitor(
db: Bindings["DB"],
name: string,
url: string,
method: string = "GET",
@@ -163,40 +150,31 @@ export async function createMonitor(
) {
const now = new Date().toISOString();
const result = await db
.prepare(
`INSERT INTO monitors
(name, url, method, interval, timeout, expected_status, headers, body, created_by, active, status, response_time, last_checked, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
)
.bind(
name,
url,
method,
interval,
timeout,
expectedStatus,
JSON.stringify(headers),
body,
userId,
1, // active
"pending",
0,
null,
now,
now
)
.run();
const newMonitor = await db
.insert(monitors)
.values({
name: name,
url: url,
method: method,
interval: interval,
timeout: timeout,
expected_status: expectedStatus,
headers: JSON.stringify(headers),
body: body,
created_by: userId,
active: 1,
status: "pending",
response_time: 0,
last_checked: null,
created_at: now,
updated_at: now,
})
.returning();
if (!result.success) {
if (!newMonitor) {
throw new Error("创建监控失败");
}
// 获取新创建的监控,解析 headers 字段
const newMonitor = await db
.prepare("SELECT * FROM monitors WHERE rowid = last_insert_rowid()")
.first<Monitor>();
if (newMonitor && typeof newMonitor.headers === "string") {
try {
newMonitor.headers = JSON.parse(newMonitor.headers);
@@ -210,7 +188,6 @@ export async function createMonitor(
// 更新监控配置
export async function updateMonitorConfig(
db: Bindings["DB"],
id: number,
updates: Partial<Monitor>
) {
@@ -276,21 +253,24 @@ export async function updateMonitorConfig(
values.push(id);
// 执行更新
const result = await db
.prepare(`UPDATE monitors SET ${fields.join(", ")} WHERE id = ?`)
.bind(...values)
.run();
const updatedMonitor = await db
.update(monitors)
.set({
name: updates.name,
url: updates.url,
method: updates.method,
interval: updates.interval,
timeout: updates.timeout,
expected_status: updates.expected_status,
headers: JSON.stringify(updates.headers),
})
.where(eq(monitors.id, id))
.returning();
if (!result.success) {
if (!updatedMonitor) {
throw new Error("更新监控失败");
}
// 获取更新后的监控
const updatedMonitor = await db
.prepare("SELECT * FROM monitors WHERE id = ?")
.bind(id)
.first<Monitor>();
// 解析 headers 字段
if (updatedMonitor && typeof updatedMonitor.headers === "string") {
try {
@@ -304,59 +284,34 @@ export async function updateMonitorConfig(
}
// 删除监控
export async function deleteMonitor(db: Bindings["DB"], id: number) {
export async function deleteMonitor(id: number) {
// 先删除关联的历史数据
await db
.prepare("DELETE FROM monitor_status_history_24h WHERE monitor_id = ?")
.bind(id)
.run();
.delete(monitorStatusHistory24h)
.where(eq(monitorStatusHistory24h.monitor_id, id));
// 删除每日统计数据
await db
.prepare("DELETE FROM monitor_daily_stats WHERE monitor_id = ?")
.bind(id)
.run();
.delete(monitorDailyStats)
.where(eq(monitorDailyStats.monitor_id, id));
// 执行删除监控
return await db.prepare("DELETE FROM monitors WHERE id = ?").bind(id).run();
return await db.delete(monitors).where(eq(monitors.id, id));
}
export async function getMonitorDailyStatsById(db: Bindings["DB"], id: number) {
export async function getMonitorDailyStatsById(id: number) {
// 查询每日统计数据
const result = await db
.prepare(
`
SELECT
date,
total_checks,
up_checks,
down_checks,
avg_response_time,
min_response_time,
max_response_time,
availability
FROM
monitor_daily_stats
WHERE
monitor_id = ?
ORDER BY
date ASC
`
)
.bind(id)
.all<MonitorDailyStats>();
return result;
}
/**
* 获取所有监控的每日统计数据
* @param db 数据库连接
* @returns 所有监控的每日统计数据和操作结果
*/
export async function getAllMonitorDailyStats(db: Bindings["DB"]) {
return await db
.prepare("SELECT * FROM monitor_daily_stats ORDER BY date ASC")
.all<MonitorDailyStats>();
.select()
.from(monitorDailyStats)
.where(eq(monitorDailyStats.monitor_id, id))
.orderBy(asc(monitorDailyStats.date));
}
export async function getAllMonitorDailyStats() {
return await db
.select()
.from(monitorDailyStats)
.orderBy(asc(monitorDailyStats.date));
}
+198 -183
View File
@@ -4,57 +4,58 @@ import {
NotificationSettings,
NotificationHistory,
NotificationConfig,
D1Meta,
D1Database,
} from "../models";
import { db } from "../config";
import {
notificationChannels,
notificationTemplates,
notificationSettings,
notificationHistory,
} from "../db/schema";
import { eq, desc, asc, and, count } from "drizzle-orm";
// 获取所有通知渠道
export const getNotificationChannels = async (
db: D1Database
): Promise<NotificationChannel[]> => {
export const getNotificationChannels = async (): Promise<
NotificationChannel[]
> => {
const result = await db
.prepare("SELECT * FROM notification_channels ORDER BY id")
.all<NotificationChannel>();
return result.results || [];
.select()
.from(notificationChannels)
.orderBy(asc(notificationChannels.id));
return result || [];
};
// 根据ID获取通知渠道
export const getNotificationChannelById = async (
db: D1Database,
id: number
): Promise<NotificationChannel | null> => {
const result = await db
.prepare("SELECT * FROM notification_channels WHERE id = ?")
.bind(id)
.first<NotificationChannel>();
.select()
.from(notificationChannels)
.where(eq(notificationChannels.id, id));
return result;
};
// 创建通知渠道
export const createNotificationChannel = async (
db: D1Database,
channel: Omit<NotificationChannel, "id" | "created_at" | "updated_at">
): Promise<number> => {
const result = await db
.prepare(
`INSERT INTO notification_channels (name, type, config, enabled, created_by)
VALUES (?, ?, ?, ?, ?)`
)
.bind(
channel.name,
channel.type,
channel.config,
channel.enabled ? 1 : 0,
channel.created_by
)
.run();
.insert(notificationChannels)
.values({
name: channel.name,
type: channel.type,
config: channel.config,
enabled: channel.enabled ? 1 : 0,
created_by: channel.created_by,
})
.returning();
return (result.meta as D1Meta)?.last_row_id || 0;
return result[0].id;
};
// 更新通知渠道
export const updateNotificationChannel = async (
db: D1Database,
id: number,
channel: Partial<
Omit<NotificationChannel, "id" | "created_at" | "updated_at">
@@ -91,32 +92,34 @@ export const updateNotificationChannel = async (
values.push(id);
const result = await db
.prepare(`UPDATE notification_channels SET ${sets.join(", ")} WHERE id = ?`)
.bind(...values)
.run();
.update(notificationChannels)
.set({
name: channel.name,
type: channel.type,
config: channel.config,
enabled: channel.enabled ? 1 : 0,
updated_at: new Date().toISOString(),
})
.where(eq(notificationChannels.id, id));
return ((result.meta as D1Meta)?.changes || 0) > 0;
return result.length > 0;
};
// 删除通知渠道
export const deleteNotificationChannel = async (
db: D1Database,
id: number
): Promise<boolean> => {
// 先删除通知历史记录表中的关联记录
await db
.prepare("DELETE FROM notification_history WHERE channel_id = ?")
.bind(id)
.run();
.delete(notificationHistory)
.where(eq(notificationHistory.channel_id, id));
// 再检查并更新通知设置中的channels列表
const allSettings = await db
.prepare("SELECT id, channels FROM notification_settings")
.all<{ id: number; channels: string }>();
const allSettings = await db.select().from(notificationSettings);
// 遍历所有设置,从channels列表中移除要删除的渠道ID
if (allSettings.results && allSettings.results.length > 0) {
for (const setting of allSettings.results) {
if (allSettings && allSettings.length > 0) {
for (const setting of allSettings) {
try {
const channelsList = JSON.parse(setting.channels || "[]");
const newChannelsList = channelsList.filter(
@@ -126,11 +129,11 @@ export const deleteNotificationChannel = async (
// 如果列表变化了,更新数据库
if (JSON.stringify(channelsList) !== JSON.stringify(newChannelsList)) {
await db
.prepare(
"UPDATE notification_settings SET channels = ? WHERE id = ?"
)
.bind(JSON.stringify(newChannelsList), setting.id)
.run();
.update(notificationSettings)
.set({
channels: JSON.stringify(newChannelsList),
})
.where(eq(notificationSettings.id, setting.id));
}
} catch (error) {
console.error("解析通知设置渠道列表出错:", error);
@@ -140,63 +143,58 @@ export const deleteNotificationChannel = async (
// 最后删除通知渠道本身
const result = await db
.prepare("DELETE FROM notification_channels WHERE id = ?")
.bind(id)
.run();
.delete(notificationChannels)
.where(eq(notificationChannels.id, id));
return ((result.meta as D1Meta)?.changes || 0) > 0;
return result.length > 0;
};
// 获取所有通知模板
export const getNotificationTemplates = async (
db: D1Database
): Promise<NotificationTemplate[]> => {
export const getNotificationTemplates = async (): Promise<
NotificationTemplate[]
> => {
const result = await db
.prepare(
"SELECT * FROM notification_templates ORDER BY is_default DESC, id"
)
.all<NotificationTemplate>();
return result.results || [];
.select()
.from(notificationTemplates)
.orderBy(
desc(notificationTemplates.is_default),
asc(notificationTemplates.id)
);
return result || [];
};
// 根据ID获取通知模板
export const getNotificationTemplateById = async (
db: D1Database,
id: number
): Promise<NotificationTemplate | null> => {
const result = await db
.prepare("SELECT * FROM notification_templates WHERE id = ?")
.bind(id)
.first<NotificationTemplate>();
.select()
.from(notificationTemplates)
.where(eq(notificationTemplates.id, id));
return result;
};
// 创建通知模板
export const createNotificationTemplate = async (
db: D1Database,
template: Omit<NotificationTemplate, "id" | "created_at" | "updated_at">
): Promise<number> => {
const result = await db
.prepare(
`INSERT INTO notification_templates (name, type, subject, content, is_default, created_by)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(
template.name,
template.type,
template.subject,
template.content,
template.is_default ? 1 : 0,
template.created_by
)
.run();
.insert(notificationTemplates)
.values({
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
is_default: template.is_default ? 1 : 0,
created_by: template.created_by,
})
.returning();
return (result.meta as D1Meta)?.last_row_id || 0;
return result[0].id;
};
// 更新通知模板
export const updateNotificationTemplate = async (
db: D1Database,
id: number,
template: Partial<
Omit<NotificationTemplate, "id" | "created_at" | "updated_at">
@@ -238,44 +236,44 @@ export const updateNotificationTemplate = async (
values.push(id);
const result = await db
.prepare(
`UPDATE notification_templates SET ${sets.join(", ")} WHERE id = ?`
)
.bind(...values)
.run();
.update(notificationTemplates)
.set({
name: template.name,
type: template.type,
subject: template.subject,
content: template.content,
is_default: template.is_default ? 1 : 0,
})
.where(eq(notificationTemplates.id, id));
return ((result.meta as D1Meta)?.changes || 0) > 0;
return result.length > 0;
};
// 删除通知模板
export const deleteNotificationTemplate = async (
db: D1Database,
id: number
): Promise<boolean> => {
const result = await db
.prepare("DELETE FROM notification_templates WHERE id = ?")
.bind(id)
.run();
.delete(notificationTemplates)
.where(eq(notificationTemplates.id, id));
return ((result.meta as D1Meta)?.changes || 0) > 0;
return result.length > 0;
};
// 获取全局通知设置
export const getGlobalSettings = async (
db: D1Database
): Promise<{
export const getGlobalSettings = async (): Promise<{
monitorSettings: NotificationSettings | null;
agentSettings: NotificationSettings | null;
}> => {
const monitorSettings = await db
.prepare("SELECT * FROM notification_settings WHERE target_type = ?")
.bind("global-monitor")
.first<NotificationSettings>();
.select()
.from(notificationSettings)
.where(eq(notificationSettings.target_type, "global-monitor"));
const agentSettings = await db
.prepare("SELECT * FROM notification_settings WHERE target_type = ?")
.bind("global-agent")
.first<NotificationSettings>();
.select()
.from(notificationSettings)
.where(eq(notificationSettings.target_type, "global-agent"));
return {
monitorSettings,
@@ -285,37 +283,40 @@ export const getGlobalSettings = async (
// 获取特定对象的通知设置
export const getSpecificSettings = async (
db: D1Database,
targetType: "monitor" | "agent",
targetId?: number
): Promise<NotificationSettings[]> => {
let query = "SELECT * FROM notification_settings WHERE target_type = ?";
const params: any[] = [targetType];
if (targetId !== undefined) {
query += " AND target_id = ?";
params.push(targetId);
return await db
.select()
.from(notificationSettings)
.where(
and(
eq(notificationSettings.target_type, targetType),
eq(notificationSettings.target_id, targetId)
)
);
}
const result = await db
.prepare(query)
.bind(...params)
.all<NotificationSettings>();
return result.results || [];
return await db
.select()
.from(notificationSettings)
.where(eq(notificationSettings.target_type, targetType));
};
// 创建或更新通知设置
export const createOrUpdateSettings = async (
db: D1Database,
settings: Omit<NotificationSettings, "id" | "created_at" | "updated_at">
): Promise<number> => {
// 先检查是否已存在相同的设置
const existingSettings = await db
.prepare(
"SELECT id FROM notification_settings WHERE target_type = ? AND (target_id = ? OR (target_id IS NULL AND ? IS NULL))"
)
.bind(settings.target_type, settings.target_id, settings.target_id)
.first<{ id: number }>();
.select()
.from(notificationSettings)
.where(
and(
eq(notificationSettings.target_type, settings.target_type),
eq(notificationSettings.target_id, settings.target_id)
)
);
if (existingSettings) {
// 如果存在则更新
@@ -339,11 +340,22 @@ export const createOrUpdateSettings = async (
values.push(existingSettings.id);
await db
.prepare(
`UPDATE notification_settings SET ${sets.join(", ")} WHERE id = ?`
)
.bind(...values)
.run();
.update(notificationSettings)
.set({
enabled: settings.enabled ? 1 : 0,
on_down: settings.on_down ? 1 : 0,
on_recovery: settings.on_recovery ? 1 : 0,
on_offline: settings.on_offline ? 1 : 0,
on_cpu_threshold: settings.on_cpu_threshold ? 1 : 0,
cpu_threshold: settings.cpu_threshold,
on_memory_threshold: settings.on_memory_threshold ? 1 : 0,
memory_threshold: settings.memory_threshold,
on_disk_threshold: settings.on_disk_threshold ? 1 : 0,
disk_threshold: settings.disk_threshold,
channels: settings.channels,
updated_at: new Date().toISOString(),
})
.where(eq(notificationSettings.id, existingSettings.id));
return existingSettings.id;
} else {
@@ -364,54 +376,56 @@ export const createOrUpdateSettings = async (
});
const result = await db
.prepare(
`INSERT INTO notification_settings (${keys.join(
", "
)}) VALUES (${placeholders.join(", ")})`
)
.bind(...values)
.run();
.insert(notificationSettings)
.values({
enabled: settings.enabled ? 1 : 0,
on_down: settings.on_down ? 1 : 0,
on_recovery: settings.on_recovery ? 1 : 0,
on_offline: settings.on_offline ? 1 : 0,
on_cpu_threshold: settings.on_cpu_threshold ? 1 : 0,
cpu_threshold: settings.cpu_threshold,
on_memory_threshold: settings.on_memory_threshold ? 1 : 0,
memory_threshold: settings.memory_threshold,
on_disk_threshold: settings.on_disk_threshold ? 1 : 0,
disk_threshold: settings.disk_threshold,
channels: settings.channels,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
.returning();
return (result.meta as D1Meta)?.last_row_id || 0;
return result[0].id;
}
};
// 记录通知历史
export const createNotificationHistory = async (
db: D1Database,
history: Omit<NotificationHistory, "id" | "sent_at">
): Promise<number> => {
const result = await db
.prepare(
`INSERT INTO notification_history
(type, target_id, channel_id, template_id, status, content, error)
VALUES (?, ?, ?, ?, ?, ?, ?)`
)
.bind(
history.type,
history.target_id,
history.channel_id,
history.template_id,
history.status,
history.content,
history.error
)
.run();
.insert(notificationHistory)
.values({
type: history.type,
target_id: history.target_id,
channel_id: history.channel_id,
template_id: history.template_id,
status: history.status,
content: history.content,
error: history.error,
})
.returning();
return (result.meta as D1Meta)?.last_row_id || 0;
return result[0].id;
};
// 获取通知历史记录
export const getNotificationHistory = async (
db: D1Database,
filter: {
type?: string | undefined;
targetId?: number | undefined;
status?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
}
): Promise<{ total: number; records: NotificationHistory[] }> => {
export const getNotificationHistory = async (filter: {
type?: string | undefined;
targetId?: number | undefined;
status?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
}): Promise<{ total: number; records: NotificationHistory[] }> => {
// 构建查询
let whereConditions = [];
let whereParams = [];
@@ -435,55 +449,56 @@ export const getNotificationHistory = async (
whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
// 查询总数
const countQuery = `SELECT COUNT(*) as count FROM notification_history ${whereClause}`;
const countResult = await db
.prepare(countQuery)
.bind(...whereParams)
.first<{ count: number }>();
.select({ count: count() })
.from(notificationHistory)
.where(whereClause);
const total = countResult?.count || 0;
// 查询记录
const recordsQuery = `
SELECT * FROM notification_history
${whereClause}
ORDER BY sent_at DESC
LIMIT ? OFFSET ?
`;
// 构建参数数组
const queryParams = [...whereParams, filter.limit || 10, filter.offset || 0];
const recordsResult = await db
.prepare(recordsQuery)
.bind(...queryParams)
.all<NotificationHistory>();
.select()
.from(notificationHistory)
.where(whereClause)
.orderBy(desc(notificationHistory.sent_at))
.limit(filter.limit || 10)
.offset(filter.offset || 0);
return {
total,
records: recordsResult.results || [],
records: recordsResult || [],
};
};
// 获取完整的通知配置
export const getNotificationConfig = async (
db: D1Database,
): Promise<NotificationConfig> => {
export const getNotificationConfig = async (): Promise<NotificationConfig> => {
// 获取所有渠道
const channels = await getNotificationChannels(db);
const channels = await getNotificationChannels();
console.log("channels", channels);
// 获取所有模板
const templates = await getNotificationTemplates(db);
const templates = await getNotificationTemplates();
console.log("templates", templates);
// 获取全局设置
const globalSettings = await getGlobalSettings(db);
const globalSettings = await getGlobalSettings();
console.log("globalSettings", globalSettings);
// 获取特定监控项设置
const monitorSettings = await getSpecificSettings(db, "monitor");
const monitorSettings = await getSpecificSettings("monitor");
console.log("monitorSettings", monitorSettings);
// 获取特定客户端设置
const agentSettings = await getSpecificSettings(db, "agent");
const agentSettings = await getSpecificSettings("agent");
// 构建通知配置
console.log("agentSettings", agentSettings);
// 构建通知配置
const config: NotificationConfig = {
channels: channels,
templates: templates,
@@ -517,7 +532,7 @@ export const getNotificationConfig = async (
enabled: globalSettings.monitorSettings.enabled,
onDown: globalSettings.monitorSettings.on_down,
onRecovery: globalSettings.monitorSettings.on_recovery,
channels: JSON.parse(globalSettings.monitorSettings.channels),
channels: JSON.parse(globalSettings.monitorSettings.channels || "[]"),
};
}
@@ -532,7 +547,7 @@ export const getNotificationConfig = async (
memoryThreshold: globalSettings.agentSettings.memory_threshold,
onDiskThreshold: globalSettings.agentSettings.on_disk_threshold,
diskThreshold: globalSettings.agentSettings.disk_threshold,
channels: JSON.parse(globalSettings.agentSettings.channels),
channels: JSON.parse(globalSettings.agentSettings.channels || "[]"),
};
}
+62 -97
View File
@@ -1,62 +1,51 @@
import { StatusPageConfig, Agent, Bindings } from "../models";
import { db } from "../config";
import {
statusPageConfig,
statusPageMonitors,
statusPageAgents,
} from "../db/schema";
import { eq, desc, asc, and, count, sql } from "drizzle-orm";
/**
* 状态页相关的数据库操作
*/
// 获取所有状态页配置
export async function getAllStatusPageConfigs(db: Bindings["DB"]) {
return await db
.prepare("SELECT * FROM status_page_config")
.all<StatusPageConfig>();
export async function getAllStatusPageConfigs() {
return await db.select().from(statusPageConfig);
}
// 获取配置的监控项
export async function getConfigMonitors(
db: Bindings["DB"],
configId: number,
userId: number
) {
return await db
.prepare(
"SELECT m.id, m.name, CASE WHEN spm.monitor_id IS NOT NULL THEN 1 ELSE 0 END as selected " +
"FROM monitors m " +
"LEFT JOIN status_page_monitors spm ON m.id = spm.monitor_id AND spm.config_id = ? " +
"WHERE m.created_by = ?"
)
.bind(configId, userId)
.all<{ id: number; name: string; selected: number }>();
.select()
.from(statusPageMonitors)
.where(eq(statusPageMonitors.config_id, configId));
}
// 获取配置的客户端
export async function getConfigAgents(
db: Bindings["DB"],
configId: number,
userId: number
) {
return await db
.prepare(
"SELECT a.id, a.name, CASE WHEN spa.agent_id IS NOT NULL THEN 1 ELSE 0 END as selected " +
"FROM agents a " +
"LEFT JOIN status_page_agents spa ON a.id = spa.agent_id AND spa.config_id = ? " +
"WHERE a.created_by = ?"
)
.bind(configId, userId)
.all<{ id: number; name: string; selected: number }>();
return await db.select().from(statusPageAgents).where(eq(statusPageAgents.config_id, configId));
}
// 检查状态页配置是否存在
export async function getStatusPageConfigById(db: Bindings["DB"], id: number) {
return await db
.prepare("SELECT id FROM status_page_config WHERE id = ?")
.bind(id)
.first<{ id: number }>();
// 获取状态页配置
export async function getStatusPageConfigById(id: number) {
const config = await db
.select()
.from(statusPageConfig)
.where(eq(statusPageConfig.id, id));
return config[0];
}
// 更新状态页配置
export async function updateStatusPageConfig(
db: Bindings["DB"],
id: number,
title: string,
description: string,
@@ -64,106 +53,82 @@ export async function updateStatusPageConfig(
customCss: string
) {
return await db
.prepare(
"UPDATE status_page_config SET title = ?, description = ?, logo_url = ?, custom_css = ? WHERE id = ?"
)
.bind(title, description, logoUrl, customCss, id)
.run();
.update(statusPageConfig)
.set({
title: title,
description: description,
logo_url: logoUrl,
custom_css: customCss,
})
.where(eq(statusPageConfig.id, id));
}
// 创建状态页配置
export async function createStatusPageConfig(
db: Bindings["DB"],
userId: number,
title: string,
description: string,
logoUrl: string,
customCss: string
) {
const result = await db
.prepare(
"INSERT INTO status_page_config (user_id, title, description, logo_url, custom_css) VALUES (?, ?, ?, ?, ?)"
)
.bind(userId, title, description, logoUrl, customCss)
.run();
const result = await db.insert(statusPageConfig).values({
user_id: userId,
title: title,
description: description,
logo_url: logoUrl,
custom_css: customCss,
});
if (!result.success) {
throw new Error("创建状态页配置失败");
}
// 获取新插入的ID
return await db
.prepare("SELECT last_insert_rowid() as id")
.first<{ id: number }>();
return result.meta.last_row_id;
}
// 清除配置的监控项关联
export async function clearConfigMonitorLinks(
db: Bindings["DB"],
configId: number
) {
export async function clearConfigMonitorLinks(configId: number) {
return await db
.prepare("DELETE FROM status_page_monitors WHERE config_id = ?")
.bind(configId)
.run();
.delete(statusPageMonitors)
.where(eq(statusPageMonitors.config_id, configId));
}
// 清除配置的客户端关联
export async function clearConfigAgentLinks(
db: Bindings["DB"],
configId: number
) {
export async function clearConfigAgentLinks(configId: number) {
return await db
.prepare("DELETE FROM status_page_agents WHERE config_id = ?")
.bind(configId)
.run();
.delete(statusPageAgents)
.where(eq(statusPageAgents.config_id, configId));
}
// 添加监控项到配置
export async function addMonitorToConfig(
db: Bindings["DB"],
configId: number,
monitorId: number
) {
return await db
.prepare(
"INSERT INTO status_page_monitors (config_id, monitor_id) VALUES (?, ?)"
)
.bind(configId, monitorId)
.run();
export async function addMonitorToConfig(configId: number, monitorId: number) {
return await db.insert(statusPageMonitors).values({
config_id: configId,
monitor_id: monitorId,
});
}
// 添加客户端到配置
export async function addAgentToConfig(
db: Bindings["DB"],
configId: number,
agentId: number
) {
return await db
.prepare(
"INSERT INTO status_page_agents (config_id, agent_id) VALUES (?, ?)"
)
.bind(configId, agentId)
.run();
export async function addAgentToConfig(configId: number, agentId: number) {
return await db.insert(statusPageAgents).values({
config_id: configId,
agent_id: agentId,
});
}
// 获取选中的监控项IDs
export async function getSelectedMonitors(
db: Bindings["DB"],
configId: number
) {
export async function getSelectedMonitors(configId: number) {
return await db
.prepare("SELECT monitor_id FROM status_page_monitors WHERE config_id = ?")
.bind(configId)
.all<{ monitor_id: number }>();
.select({ monitor_id: statusPageMonitors.monitor_id })
.from(statusPageMonitors)
.where(eq(statusPageMonitors.config_id, configId));
}
// 获取选中的客户端IDs
export async function getSelectedAgents(db: Bindings["DB"], configId: number) {
export async function getSelectedAgents(configId: number) {
return await db
.prepare("SELECT agent_id FROM status_page_agents WHERE config_id = ?")
.bind(configId)
.all<{ agent_id: number }>();
.select({ agent_id: statusPageAgents.agent_id })
.from(statusPageAgents)
.where(eq(statusPageAgents.config_id, configId));
}
+86 -74
View File
@@ -1,5 +1,9 @@
import { eq, asc, and } from "drizzle-orm";
import { Bindings } from "../models/db";
import { User } from "../models";
import { db } from "../config";
import { users } from "../db/schema";
// 不含密码的用户信息
type UserWithoutPassword = Omit<User, "password">;
@@ -9,55 +13,60 @@ type UserWithoutPassword = Omit<User, "password">;
*/
// 获取所有用户(不包括密码)
export async function getAllUsers(db: Bindings["DB"]) {
export async function getAllUsers() {
return await db
.prepare(
"SELECT id, username, email, role, created_at, updated_at FROM users ORDER BY id"
)
.all<UserWithoutPassword>();
.select({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
created_at: users.created_at,
updated_at: users.updated_at,
})
.from(users)
.orderBy(asc(users.id))
.then((result: User[]) => result);
}
// 根据ID获取用户(不包括密码)
export async function getUserById(db: Bindings["DB"], id: number) {
export async function getUserById(id: number) {
return await db
.prepare(
"SELECT id, username, email, role, created_at, updated_at FROM users WHERE id = ?"
)
.bind(id)
.first<UserWithoutPassword>();
.select({
id: users.id,
username: users.username,
email: users.email,
role: users.role,
created_at: users.created_at,
updated_at: users.updated_at,
})
.from(users)
.where(eq(users.id, id))
.limit(1)
.then((result: User[]) => result[0] || null);
}
// 根据ID获取完整用户信息(包括密码)
export async function getFullUserById(db: Bindings["DB"], id: number) {
export async function getFullUserById(id: number) {
return await db
.prepare("SELECT * FROM users WHERE id = ?")
.bind(id)
.first<User>();
.select()
.from(users)
.where(eq(users.id, id))
.limit(1)
.then((result: User[]) => result[0] || null);
}
// 根据用户名检查用户是否存在
export async function checkUserExists(
db: Bindings["DB"],
username: string,
excludeId?: number
) {
let query = "SELECT id FROM users WHERE username = ?";
let params: any[] = [username];
if (excludeId !== undefined) {
query += " AND id != ?";
params.push(excludeId);
}
export async function checkUserExists(username: string) {
return await db
.prepare(query)
.bind(...params)
.first<{ id: number } | null>();
.select({ id: users.id })
.from(users)
.where(eq(users.username, username))
.limit(1)
.then((result: User[]) => result[0] || null);
}
// 创建新用户
export async function createUser(
db: Bindings["DB"],
username: string,
hashedPassword: string,
email: string | null,
@@ -66,27 +75,26 @@ export async function createUser(
const now = new Date().toISOString();
const result = await db
.prepare(
`INSERT INTO users (username, password, email, role, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)`
)
.bind(username, hashedPassword, email, role, now, now)
.run();
.insert(users)
.values({
username: username,
password: hashedPassword,
email: email,
role: role,
created_at: now,
updated_at: now,
})
.returning();
if (!result.success) {
throw new Error("创建用户失败");
}
return await db
.prepare(
"SELECT id, username, email, role, created_at, updated_at FROM users WHERE rowid = last_insert_rowid()"
)
.first<UserWithoutPassword>();
return result[0];
}
// 更新用户信息
export async function updateUser(
db: Bindings["DB"],
id: number,
updates: {
username?: string;
@@ -134,29 +142,36 @@ export async function updateUser(
// 执行更新
const result = await db
.prepare(`UPDATE users SET ${fieldsToUpdate.join(", ")} WHERE id = ?`)
.bind(...values)
.run();
.update(users)
.set({
username: updates.username,
email: updates.email,
role: updates.role,
password: updates.password,
updated_at: now,
})
.where(eq(users.id, id))
.returning();
if (!result.success) {
throw new Error("更新用户失败");
}
return await getUserById(db, id);
return result[0];
}
// 更新用户密码
export async function updateUserPassword(
db: Bindings["DB"],
id: number,
hashedPassword: string
) {
export async function updateUserPassword(id: number, hashedPassword: string) {
const now = new Date().toISOString();
const result = await db
.prepare("UPDATE users SET password = ?, updated_at = ? WHERE id = ?")
.bind(hashedPassword, now, id)
.run();
.update(users)
.set({
password: hashedPassword,
updated_at: now,
})
.where(eq(users.id, id))
.returning();
if (!result.success) {
throw new Error("更新密码失败");
@@ -166,11 +181,8 @@ export async function updateUserPassword(
}
// 删除用户
export async function deleteUser(db: Bindings["DB"], id: number) {
const result = await db
.prepare("DELETE FROM users WHERE id = ?")
.bind(id)
.run();
export async function deleteUser(id: number) {
const result = await db.delete(users).where(eq(users.id, id)).returning();
if (!result.success) {
throw new Error("删除用户失败");
@@ -181,26 +193,26 @@ export async function deleteUser(db: Bindings["DB"], id: number) {
// 根据用户名获取用户
export async function getUserByUsername(
db: Bindings["DB"],
username: string
): Promise<User | null> {
return await db
.prepare("SELECT * FROM users WHERE username = ?")
.bind(username)
.first<User | null>();
.select()
.from(users)
.where(eq(users.username, username))
.limit(1)
.then((result: User[]) => result[0] || null);
}
// 获取管理员用户ID
export async function getAdminUserId(db: Bindings["DB"]) {
const adminUser = await db
.prepare("SELECT id FROM users WHERE role = ?")
.bind("admin")
.first<{ id: number }>();
export async function getAdminUserId() {
const adminId = await db
.select({ id: users.id })
.from(users)
.where(eq(users.username, "admin"));
if (!adminUser) {
if (!adminId) {
throw new Error("无法找到管理员用户");
}
return adminUser.id;
}
return adminId[0].id;
}
+63 -165
View File
@@ -8,14 +8,12 @@ import { handleAgentThresholdNotification } from "../jobs/agent-task";
* @param db 数据库连接
* @returns 客户端列表
*/
export async function getAgents(
db: Bindings["DB"],
) {
export async function getAgents() {
try {
const result = await AgentRepository.getAllAgents(db);
const result = await AgentRepository.getAllAgents();
return {
success: true,
agents: result.results || [],
agents: result || [],
status: 200,
};
} catch (error) {
@@ -35,70 +33,23 @@ export async function getAgents(
* @param agentId 客户端ID
* @returns 客户端详情
*/
export async function getAgentDetail(db: Bindings["DB"], agentId: number) {
export async function getAgentDetail(agentId: number) {
// 获取客户端信息
const agent = await AgentRepository.getAgentById(db, agentId);
const agent = await AgentRepository.getAgentById(agentId);
if (!agent) {
return { success: false, message: "客户端不存在", status: 404 };
throw new Error("客户端不存在");
}
// 不返回令牌,但保留其他所有字段
const { token, ...rest } = agent;
return {
success: true,
agent: {
...rest,
ip_addresses: getFormattedIPAddresses(rest.ip_addresses as any),
},
status: 200,
ip_addresses: getFormattedIPAddresses(agent.ip_addresses as any),
};
}
/**
* 创建新客户端
* @param db 数据库连接
* @param env 环境变量
* @param name 客户端名称
* @param userId 创建者ID
* @returns 创建结果
*/
export async function createAgentService(
db: Bindings["DB"],
env: any,
name: string,
userId: number
) {
try {
// 验证名称
if (!name || name.trim() === "") {
return { success: false, message: "客户端名称不能为空", status: 400 };
}
// 生成令牌
const token = await generateAgentToken(env);
// 创建新客户端
const newAgent = await AgentRepository.createAgent(db, name, token, userId);
return {
success: true,
message: "客户端创建成功",
agent: newAgent,
status: 201,
};
} catch (error) {
console.error("创建客户端错误:", error);
return {
success: false,
message: "创建客户端失败",
error: error instanceof Error ? error.message : String(error),
status: 500,
};
}
}
/**
* 更新客户端信息
* @param db 数据库连接
@@ -116,11 +67,11 @@ export async function updateAgentService(
os?: string;
version?: string;
status?: string;
},
}
) {
try {
// 获取当前客户端数据
const agent = await AgentRepository.getAgentById(db, agentId);
const agent = await AgentRepository.getAgentById(agentId);
if (!agent) {
return { success: false, message: "客户端不存在", status: 404 };
@@ -131,26 +82,25 @@ export async function updateAgentService(
return { success: false, message: "客户端名称不能为空", status: 400 };
}
// 执行更新
const result = await AgentRepository.updateAgent(db, agentId, updateData);
if (
typeof result === "object" &&
"message" in result &&
result.message === "没有更新任何字段"
) {
return {
success: true,
message: "没有更新任何字段",
agent,
status: 200,
};
if (updateData.ip_addresses && updateData.ip_addresses.length > 0) {
agent.ip_addresses = JSON.stringify(updateData.ip_addresses);
} else {
agent.ip_addresses = "[]";
}
agent.name = updateData.name;
agent.hostname = updateData.hostname;
agent.os = updateData.os;
agent.version = updateData.version;
agent.status = updateData.status;
// 执行更新
const updatedAgent = await AgentRepository.updateAgent(agent);
return {
success: true,
message: "客户端信息已更新",
agent: result,
agent: updatedAgent,
status: 200,
};
} catch (error) {
@@ -170,34 +120,21 @@ export async function updateAgentService(
* @param agentId 客户端ID
* @returns 删除结果
*/
export async function deleteAgentService(
db: Bindings["DB"],
agentId: number,
) {
export async function deleteAgentService( agentId: number) {
try {
// 获取客户端信息
const agent = await AgentRepository.getAgentById(db, agentId);
const agent = await AgentRepository.getAgentById(agentId);
if (!agent) {
return { success: false, message: "客户端不存在", status: 404 };
throw new Error("客户端不存在");
}
// 执行删除客户端
await AgentRepository.deleteAgent(db, agent.id);
await AgentRepository.deleteAgent(agent.id);
return {
success: true,
message: "客户端已删除",
status: 200,
};
return true;
} catch (error) {
console.error("删除客户端错误:", error);
return {
success: false,
message: "删除客户端失败",
error: error instanceof Error ? error.message : String(error),
status: 500,
};
throw error;
}
}
@@ -249,20 +186,7 @@ export function getFormattedIPAddresses(
}
}
/**
* 客户端自注册
* @param db 数据库连接
* @param env 环境变量
* @param token 注册令牌
* @param name 客户端名称
* @param hostname 主机名
* @param ipAddresses IP地址
* @param os 操作系统
* @param version 版本
* @returns 注册结果
*/
export async function registerAgentService(
db: Bindings["DB"],
env: any,
token: string,
name: string,
@@ -278,9 +202,9 @@ export async function registerAgentService(
}
// 通过token查找客户端
const existingAgent = await AgentRepository.getAgentByToken(db, token);
const existingAgent = await AgentRepository.getAgentByToken(token);
if (existingAgent) {
if (existingAgent && existingAgent.id) {
return {
success: true,
message: "客户端已存在",
@@ -304,19 +228,18 @@ export async function registerAgentService(
}
// 查找管理员用户作为客户端创建者
const adminId = await AgentRepository.getAdminUserId(db);
const adminId = await AgentRepository.getAdminUserId();
// 创建新客户端
const newAgent = await AgentRepository.createAgent(
db,
name || "New Agent",
name,
token,
adminId,
"active",
hostname || null,
os || null,
version || null,
ipAddresses
hostname || "unknown",
os || "unknown",
version || "unknown",
ipAddresses || []
);
return {
@@ -344,8 +267,6 @@ export async function registerAgentService(
* @returns 更新结果
*/
export async function updateAgentStatusService(
db: Bindings["DB"],
env: any,
status: any
) {
try {
@@ -363,12 +284,10 @@ export async function updateAgentStatusService(
console.log("norlmalInfo", norlmalInfo);
if (!norlmalInfo.token) {
return { success: false, message: "缺少API令牌", status: 400 };
throw new Error("缺少API令牌");
}
// 通过token查找客户端
const agent = await AgentRepository.getAgentByToken(db, norlmalInfo.token);
console.log("agent", agent);
const agent = await AgentRepository.getAgentByToken(norlmalInfo.token);
if (
agent.status != "active" ||
@@ -377,8 +296,16 @@ export async function updateAgentStatusService(
agent.os != norlmalInfo.os ||
agent.version != norlmalInfo.version
) {
console.log("更新客户端信息");
await AgentRepository.updateAgent(db, agent.id, norlmalInfo);
agent.ip_addresses = JSON.stringify(norlmalInfo.ip_addresses);
agent.hostname = norlmalInfo.hostname;
agent.os = norlmalInfo.os;
agent.version = norlmalInfo.version;
agent.keepalive = norlmalInfo.keepalive;
agent.status = norlmalInfo.status;
console.log("update agent info: ", agent);
await AgentRepository.updateAgent(agent);
}
// 插入 metric 信息
@@ -402,81 +329,52 @@ export async function updateAgentStatusService(
console.log("metrics", metrics);
// 插入 metric 信息到 24h 热表
for (const metric of metrics) {
console.log("准备插入指标数据:", metric);
const metricValues = [
metric.agent_id,
metric.timestamp,
metric.cpu_usage,
metric.cpu_cores,
metric.cpu_model,
metric.memory_total,
metric.memory_used,
metric.memory_free,
metric.memory_usage_rate,
metric.load_1,
metric.load_5,
metric.load_15,
metric.disk_metrics,
metric.network_metrics,
];
console.log("指标数据值:", metricValues);
const result = await AgentRepository.insertAgentMetrics(db, metricValues);
console.log("插入指标结果:", result);
}
const result = await AgentRepository.insertAgentMetrics(metrics);
console.log("插入指标结果:", result);
// 取出 metrics中的最新一条数据用于通知
const latestMetric = metrics[metrics.length - 1];
console.log("latestMetric cpu", latestMetric.cpu_usage);
console.log("latestMetric memory", latestMetric.memory_usage_rate);
await handleAgentThresholdNotification(
env,
agent.id,
"cpu",
latestMetric.cpu_usage
);
await handleAgentThresholdNotification(
env,
agent.id,
"memory",
latestMetric.memory_usage_rate
);
return {
success: true,
message: "客户端状态已更新",
agentId: agent.id,
status: 200,
};
} catch (error) {
console.error("更新客户端状态错误:", error);
return {
success: false,
message: "更新客户端状态失败",
error: error instanceof Error ? error.message : String(error),
};
throw error;
}
}
export async function getAgentById(db: Bindings["DB"], id: number) {
return await AgentRepository.getAgentById(db, id);
export async function getAgentById(id: number) {
return await AgentRepository.getAgentById(id);
}
export async function getActiveAgents(env: any) {
return await AgentRepository.getActiveAgents(env.DB);
return await AgentRepository.getActiveAgents();
}
export async function setAgentInactive(env: any, id: number) {
return await AgentRepository.setAgentInactive(env.DB, id);
return await AgentRepository.setAgentInactive(id);
}
export async function getAgentMetrics(db: Bindings["DB"], agentId: number) {
return await AgentRepository.getAgentMetrics(db, agentId);
export async function getAgentMetrics( agentId: number) {
return await AgentRepository.getAgentMetrics(agentId);
}
export async function getLatestAgentMetrics(db: Bindings["DB"], agentId: number) {
return await AgentRepository.getLatestAgentMetrics(db, agentId);
}
export async function getLatestAgentMetrics(
agentId: number
) {
return await AgentRepository.getLatestAgentMetrics(agentId);
}
+3 -15
View File
@@ -38,21 +38,10 @@ export async function loginUser(
2
)
);
console.log("env 中的所有键:", Object.keys(env));
if (env.CF_VERSION_METADATA) {
console.log("env.CF_VERSION_METADATA 存在");
console.log(
"CF_VERSION_METADATA 内容:",
JSON.stringify(env.CF_VERSION_METADATA, null, 2)
);
} else {
console.error("错误: env.CF_VERSION_METADATA 不存在");
}
// 查找用户
console.log("开始查找用户:", username);
const user = await repositories.getUserByUsername(env.DB, username);
const user = await repositories.getUserByUsername(username);
if (!user) {
console.log("用户不存在:", username);
@@ -132,7 +121,7 @@ export async function registerUser(
): Promise<{ success: boolean; message: string; user?: any }> {
try {
// 检查用户名是否已存在
const existingUser = await repositories.getUserByUsername(env.DB, username);
const existingUser = await repositories.getUserByUsername(username);
if (existingUser) {
return { success: false, message: "用户名已存在" };
}
@@ -143,7 +132,6 @@ export async function registerUser(
// 创建用户
const newUser = await repositories.createUser(
env.DB,
username,
hashedPassword,
email,
@@ -172,7 +160,7 @@ export async function getCurrentUser(
userId: number
): Promise<{ success: boolean; message: string; user?: any }> {
try {
const user = await repositories.getUserById(env.DB, userId);
const user = await repositories.getUserById(userId);
if (!user) {
return { success: false, message: "用户不存在" };
+6 -6
View File
@@ -1,12 +1,12 @@
import { Bindings } from "../models";
import { Monitor } from "../models";
import * as repositories from "../repositories";
export async function getDashboardData(db: Bindings["DB"]) {
const monitors = await repositories.getAllMonitors(db);
const agents = await repositories.getAllAgents(db);
export async function getDashboardData() {
const monitors = await repositories.getAllMonitors();
const agents = await repositories.getAllAgents();
if (monitors.monitors && monitors.monitors.length > 0) {
monitors.monitors.forEach((monitor) => {
monitors.monitors.forEach((monitor: Monitor) => {
if (typeof monitor.headers === "string") {
try {
monitor.headers = JSON.parse(monitor.headers);
@@ -18,6 +18,6 @@ export async function getDashboardData(db: Bindings["DB"]) {
}
return {
monitors: monitors.monitors,
agents: agents.results,
agents: agents,
};
}
+58 -146
View File
@@ -1,27 +1,12 @@
import { Bindings } from "../models/db";
import * as models from "../models";
import * as repositories from "../repositories";
import * as NotificationService from "./NotificationService";
/**
* 获取所有需要检查的监控
*/
export async function getMonitorsToCheck(db: Bindings["DB"]) {
return await repositories.getMonitorsToCheck(db);
export async function getMonitorsToCheck() {
return await repositories.getMonitorsToCheck();
}
/**
* 检查单个监控的状态
* @param db 数据库连接
* @param monitor 监控配置
* @returns 监控检查结果
*/
export async function checkMonitor(
db: Bindings["DB"],
monitor: models.Monitor
) {
export async function checkMonitor(monitor: models.Monitor) {
try {
console.log(`开始检查监控项: ${monitor.name} (${monitor.url})`);
@@ -30,11 +15,9 @@ export async function checkMonitor(
const startTime = Date.now();
let response;
let headers: Headers = new Headers();
let error = null;
// 使用 headers 对象
let customHeaders = monitor.headers || {};
try {
// 设置超时
const controller = new AbortController();
@@ -43,14 +26,28 @@ export async function checkMonitor(
(monitor.timeout || 30) * 1000
);
// 如果headers是字符串,则转换为对象
if (typeof monitor.headers === "string") {
const parseHeaders = JSON.parse(monitor.headers);
if (
parseHeaders &&
typeof parseHeaders === "object" &&
!Array.isArray(parseHeaders)
) {
const headerObj = Object.assign({}, parseHeaders);
if (Object.keys(headerObj).length === 0) {
headers = new Headers();
} else {
headers = new Headers(headerObj);
}
}
}
// 发送请求
response = await fetch(monitor.url, {
method: monitor.method || "GET",
headers: customHeaders,
body:
monitor.method !== "GET" && monitor.method !== "HEAD"
? monitor.body || undefined
: undefined,
headers: headers,
body: monitor.method !== "GET" && monitor.method !== "HEAD" ? monitor.body || "" : undefined,
signal: controller.signal,
});
@@ -76,7 +73,7 @@ export async function checkMonitor(
// 检查状态码是否符合预期
let isExpectedStatus = false;
const expectedStatus = monitor.expected_status || 200;
const expectedStatus = monitor.expected_status;
// 处理范围状态码:如果预期状态码为个位数(1-5),则视为范围检查
if (expectedStatus >= 1 && expectedStatus <= 5) {
@@ -92,7 +89,6 @@ export async function checkMonitor(
// 记录状态历史
await repositories.insertMonitorStatusHistory(
db,
monitor.id,
status,
responseTime,
@@ -101,12 +97,7 @@ export async function checkMonitor(
);
// 更新监控状态
await repositories.updateMonitorStatus(
db,
monitor.id,
status,
responseTime
);
await repositories.updateMonitorStatus(monitor.id, status, responseTime);
return {
success: true,
@@ -133,68 +124,50 @@ export async function checkMonitor(
}
}
/**
* 获取所有监控(根据用户角色过滤)
* @param db 数据库连接
* @returns 监控列表和操作结果
*/
export async function getAllMonitors(db: Bindings["DB"]) {
const result = await repositories.getAllMonitors(db);
export async function getAllMonitors() {
const result = await repositories.getAllMonitors();
return {
success: true,
monitors: result.monitors || [],
monitors: result,
status: 200,
};
}
/**
* 获取单个监控详情
* @param db 数据库连接
* @param id 监控ID
* @returns 监控详情和操作结果
*/
export async function getMonitorById(db: Bindings["DB"], id: number) {
const monitor = await repositories.getMonitorById(db, id);
export async function getMonitorById(id: number) {
const monitor = await repositories.getMonitorById(id);
if (!monitor) {
return { success: false, message: "监控不存在", status: 404 };
}
// 获取历史状态数据
const historyResult = await repositories.getMonitorStatusHistoryIn24h(db, id);
const historyResult = await repositories.getMonitorStatusHistoryIn24h(id);
return {
success: true,
monitor: {
...monitor,
history: historyResult.results || [],
history: historyResult,
},
status: 200,
};
}
/**
* 创建新监控
* @param db 数据库连接
* @param data 监控数据
* @param userId 用户ID
* @returns 创建结果
*/
export async function createMonitor(
db: Bindings["DB"],
data: any,
userId: number
) {
export async function createMonitor(data: any, userId: number) {
try {
// 验证必填字段
if (!data.name || !data.url || !data.method) {
return { success: false, message: "缺少必填字段", status: 400 };
}
// 如果headers是对象,则转换为字符串
if (typeof data.headers !== "string") {
data.headers = JSON.stringify(data.headers);
}
// 创建新监控
const newMonitor = await repositories.createMonitor(
db,
data.name,
data.url,
data.method,
@@ -222,17 +195,10 @@ export async function createMonitor(
}
}
/**
* 更新监控信息
* @param db 数据库连接
* @param id 监控ID
* @param data 更新数据
* @returns 更新结果
*/
export async function updateMonitor(db: Bindings["DB"], id: number, data: any) {
export async function updateMonitor(id: number, data: any) {
try {
// 检查监控是否存在
const monitor = await repositories.getMonitorById(db, id);
const monitor = await repositories.getMonitorById(id);
if (!monitor) {
return { success: false, message: "监控不存在", status: 404 };
@@ -259,7 +225,6 @@ export async function updateMonitor(db: Bindings["DB"], id: number, data: any) {
// 执行更新
const updatedMonitor = await repositories.updateMonitorConfig(
db,
id,
updateData
);
@@ -289,23 +254,17 @@ export async function updateMonitor(db: Bindings["DB"], id: number, data: any) {
}
}
/**
* 删除监控
* @param db 数据库连接
* @param id 监控ID
* @returns 删除结果
*/
export async function deleteMonitor(db: Bindings["DB"], id: number) {
export async function deleteMonitor(id: number) {
try {
// 检查监控是否存在
const monitor = await repositories.getMonitorById(db, id);
const monitor = await repositories.getMonitorById(id);
if (!monitor) {
return { success: false, message: "监控不存在", status: 404 };
}
// 执行删除
const result = await repositories.deleteMonitor(db, id);
const result = await repositories.deleteMonitor(id);
if (!result.success) {
throw new Error("删除监控失败");
@@ -327,23 +286,14 @@ export async function deleteMonitor(db: Bindings["DB"], id: number) {
}
}
/**
* 获取监控状态历史
* @param db 数据库连接
* @param id 监控ID
* @param userId 用户ID
* @param userRole 用户角色
* @returns 状态历史
*/
export async function getMonitorStatusHistoryById(
db: Bindings["DB"],
id: number,
userId: number,
userRole: string
) {
try {
// 检查监控是否存在
const monitor = await repositories.getMonitorById(db, id);
const monitor = await repositories.getMonitorById(id);
if (!monitor) {
return { success: false, message: "监控不存在", status: 404 };
@@ -355,14 +305,11 @@ export async function getMonitorStatusHistoryById(
}
// 获取历史状态
const historyResult = await repositories.getMonitorStatusHistoryIn24h(
db,
id
);
const historyResult = await repositories.getMonitorStatusHistoryIn24h(id);
return {
success: true,
history: historyResult.results || [],
history: historyResult,
status: 200,
};
} catch (error) {
@@ -376,43 +323,26 @@ export async function getMonitorStatusHistoryById(
}
}
/**
*
* 获取所有监控状态历史
* @param db 数据库连接
* @returns 所有监控状态历史
*/
export async function getAllMonitorStatusHistory(db: Bindings["DB"]) {
const result = await repositories.getAllMonitorStatusHistoryIn24h(db);
export async function getAllMonitorStatusHistory() {
const result = await repositories.getAllMonitorStatusHistoryIn24h();
return {
success: true,
history: result.results || [],
history: result,
status: 200,
};
}
/**
* 手动检查监控并处理通知
* @param db 数据库连接
* @param id 监控ID
* @param env 环境变量
* @returns 检查结果
*/
export async function manualCheckMonitor(
db: Bindings["DB"],
id: number,
env: any
) {
export async function manualCheckMonitor(id: number) {
try {
// 检查监控是否存在
const monitor = await repositories.getMonitorById(db, id);
const monitor = await repositories.getMonitorById(id);
if (!monitor) {
return { success: false, message: "监控不存在", status: 404 };
}
// 使用抽象出来的通用检查监控函数进行检查
const result = await checkMonitor(db, monitor);
const result = await checkMonitor(monitor);
// 处理通知逻辑
try {
@@ -426,7 +356,6 @@ export async function manualCheckMonitor(
console.log(`检查通知设置...`);
const notificationCheck =
await NotificationService.shouldSendNotification(
db,
"monitor",
monitor.id,
result.previous_status,
@@ -472,7 +401,6 @@ export async function manualCheckMonitor(
// 发送通知
console.log(`开始发送通知...`);
const notificationResult = await NotificationService.sendNotification(
db,
"monitor",
monitor.id,
variables,
@@ -522,11 +450,6 @@ export async function manualCheckMonitor(
}
}
/**
* 获取预期状态码的显示文本
* @param expectedStatus 预期状态码
* @returns 显示文本
*/
function getExpectedStatusDisplay(expectedStatus: number): string {
if (expectedStatus >= 1 && expectedStatus <= 5) {
return `${expectedStatus}xx`;
@@ -534,34 +457,23 @@ function getExpectedStatusDisplay(expectedStatus: number): string {
return String(expectedStatus);
}
/**
* 获取单个监控的每日统计数据
* @param db 数据库连接
* @param id 监控ID
* @returns 监控的每日统计数据和操作结果
*/
export async function getMonitorDailyStats(db: Bindings["DB"], id: number) {
const result = await repositories.getMonitorDailyStatsById(db, id);
export async function getMonitorDailyStats(id: number) {
const result = await repositories.getMonitorDailyStatsById(id);
return {
success: true,
dailyStats: result.results,
dailyStats: result,
message: "获取监控每日统计数据成功",
status: 200,
};
}
/**
* 获取所有监控的每日统计数据
* @param db 数据库连接
* @returns 所有监控的每日统计数据和操作结果
*/
export async function getAllMonitorDailyStats(db: Bindings["DB"]) {
export async function getAllMonitorDailyStats() {
// 获取所有监控
const result = await repositories.getAllMonitorDailyStats(db);
const result = await repositories.getAllMonitorDailyStats();
return {
success: true,
dailyStats: result.results,
dailyStats: result,
message: "获取所有监控的每日统计数据成功",
status: 200,
};
+33 -63
View File
@@ -1,32 +1,24 @@
/**
* NotificationService.ts
* 处理所有与通知相关的业务逻辑
*/
import { D1Database } from "../models/db";
import * as models from "../models";
import * as repositories from "../repositories";
// 通知渠道相关服务
export async function getNotificationChannels(
db: D1Database
): Promise<models.NotificationChannel[]> {
return await repositories.getNotificationChannels(db);
export async function getNotificationChannels(): Promise<
models.NotificationChannel[]
> {
return await repositories.getNotificationChannels();
}
export async function getNotificationChannelById(
db: D1Database,
id: number
): Promise<models.NotificationChannel | null> {
return await repositories.getNotificationChannelById(db, id);
return await repositories.getNotificationChannelById(id);
}
export async function createNotificationChannel(
db: D1Database,
channel: Omit<models.NotificationChannel, "id" | "created_at" | "updated_at">
): Promise<{ success: boolean; id?: number; message?: string }> {
try {
const id = await repositories.createNotificationChannel(db, channel);
const id = await repositories.createNotificationChannel(channel);
return { success: true, id };
} catch (error) {
console.error("创建通知渠道失败:", error);
@@ -38,18 +30,13 @@ export async function createNotificationChannel(
}
export async function updateNotificationChannel(
db: D1Database,
id: number,
channel: Partial<
Omit<models.NotificationChannel, "id" | "created_at" | "updated_at">
>
): Promise<{ success: boolean; message?: string }> {
try {
const result = await repositories.updateNotificationChannel(
db,
id,
channel
);
const result = await repositories.updateNotificationChannel(id, channel);
return {
success: result,
message: result ? "通知渠道更新成功" : "通知渠道不存在或未做任何更改",
@@ -64,11 +51,10 @@ export async function updateNotificationChannel(
}
export async function deleteNotificationChannel(
db: D1Database,
id: number
): Promise<{ success: boolean; message?: string }> {
try {
const result = await repositories.deleteNotificationChannel(db, id);
const result = await repositories.deleteNotificationChannel(id);
return {
success: result,
message: result ? "通知渠道删除成功" : "通知渠道不存在",
@@ -86,28 +72,26 @@ export async function deleteNotificationChannel(
}
// 通知模板相关服务
export async function getNotificationTemplates(
db: D1Database
): Promise<models.NotificationTemplate[]> {
return await repositories.getNotificationTemplates(db);
export async function getNotificationTemplates(): Promise<
models.NotificationTemplate[]
> {
return await repositories.getNotificationTemplates();
}
export async function getNotificationTemplateById(
db: D1Database,
id: number
): Promise<models.NotificationTemplate | null> {
return await repositories.getNotificationTemplateById(db, id);
return await repositories.getNotificationTemplateById(id);
}
export async function createNotificationTemplate(
db: D1Database,
template: Omit<
models.NotificationTemplate,
"id" | "created_at" | "updated_at"
>
): Promise<{ success: boolean; id?: number; message?: string }> {
try {
const id = await repositories.createNotificationTemplate(db, template);
const id = await repositories.createNotificationTemplate(template);
return { success: true, id };
} catch (error) {
console.error("创建通知模板失败:", error);
@@ -119,18 +103,13 @@ export async function createNotificationTemplate(
}
export async function updateNotificationTemplate(
db: D1Database,
id: number,
template: Partial<
Omit<models.NotificationTemplate, "id" | "created_at" | "updated_at">
>
): Promise<{ success: boolean; message?: string }> {
try {
const result = await repositories.updateNotificationTemplate(
db,
id,
template
);
const result = await repositories.updateNotificationTemplate(id, template);
return {
success: result,
message: result ? "通知模板更新成功" : "通知模板不存在或未做任何更改",
@@ -145,11 +124,10 @@ export async function updateNotificationTemplate(
}
export async function deleteNotificationTemplate(
db: D1Database,
id: number
): Promise<{ success: boolean; message?: string }> {
try {
const result = await repositories.deleteNotificationTemplate(db, id);
const result = await repositories.deleteNotificationTemplate(id);
return {
success: result,
message: result ? "通知模板删除成功" : "通知模板不存在",
@@ -164,21 +142,18 @@ export async function deleteNotificationTemplate(
}
// 通知设置相关服务,获取所有的通知设置
export async function getNotificationConfig(
db: D1Database
): Promise<models.NotificationConfig> {
return await repositories.getNotificationConfig(db);
export async function getNotificationConfig(): Promise<models.NotificationConfig> {
return await repositories.getNotificationConfig();
}
export async function createOrUpdateSettings(
db: D1Database,
settings: Omit<
models.NotificationSettings,
"id" | "created_at" | "updated_at"
>
): Promise<{ success: boolean; id?: number; message?: string }> {
try {
const id = await repositories.createOrUpdateSettings(db, settings);
const id = await repositories.createOrUpdateSettings(settings);
return { success: true, id };
} catch (error) {
console.error("保存通知设置失败:", error);
@@ -190,17 +165,14 @@ export async function createOrUpdateSettings(
}
// 通知历史相关服务
export async function getNotificationHistory(
db: D1Database,
filter: {
type?: string | undefined;
targetId?: number | undefined;
status?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
}
): Promise<{ total: number; records: models.NotificationHistory[] }> {
return await repositories.getNotificationHistory(db, filter);
export async function getNotificationHistory(filter: {
type?: string | undefined;
targetId?: number | undefined;
status?: string | undefined;
limit?: number | undefined;
offset?: number | undefined;
}): Promise<{ total: number; records: models.NotificationHistory[] }> {
return await repositories.getNotificationHistory(filter);
}
// 从utils/notification.ts移植过来的通知发送逻辑
@@ -490,7 +462,6 @@ async function sendNotificationByChannel(
* 发送通知
*/
export async function sendNotification(
db: D1Database,
type: "monitor" | "agent" | "system",
targetId: number | null,
variables: Record<string, string>,
@@ -511,7 +482,7 @@ export async function sendNotification(
}
// 获取默认的通知模板
const templates = await repositories.getNotificationTemplates(db);
const templates = await repositories.getNotificationTemplates();
console.log(`[发送通知] 获取到${templates.length}个通知模板`);
const defaultTemplate = templates.find(
@@ -534,7 +505,7 @@ export async function sendNotification(
// 获取所有通知渠道
console.log(`[发送通知] 开始获取${channelIds.length}个通知渠道的详细信息`);
const channels = await Promise.all(
channelIds.map((id) => repositories.getNotificationChannelById(db, id))
channelIds.map((id) => repositories.getNotificationChannelById(id))
);
// 过滤掉不存在的渠道
@@ -579,7 +550,7 @@ export async function sendNotification(
);
// 记录通知历史
await repositories.createNotificationHistory(db, {
await repositories.createNotificationHistory({
type,
target_id: targetId,
channel_id: channel.id,
@@ -602,7 +573,7 @@ export async function sendNotification(
console.error(`[发送通知] 通过渠道${channel.id}发送通知失败:`, error);
// 记录错误
await repositories.createNotificationHistory(db, {
await repositories.createNotificationHistory({
type,
target_id: targetId,
channel_id: channel.id,
@@ -660,7 +631,6 @@ export async function sendNotification(
* 判断是否应该发送通知
*/
export async function shouldSendNotification(
db: D1Database,
type: "monitor" | "agent",
id: number,
prevStatus: string,
@@ -680,7 +650,7 @@ export async function shouldSendNotification(
}
// 获取此对象的特定设置
const specificSettings = await repositories.getSpecificSettings(db, type, id);
const specificSettings = await repositories.getSpecificSettings(type, id);
console.log(
`[通知触发检查] 获取到特定设置数量: ${
specificSettings ? specificSettings.length : 0
@@ -690,7 +660,7 @@ export async function shouldSendNotification(
let targetSettings = specificSettings;
// 如果没有特定设置,使用全局设置
if (targetSettings.length === 0) {
const globalSettings = await repositories.getGlobalSettings(db);
const globalSettings = await repositories.getGlobalSettings();
console.log(
`[通知触发检查] 获取全局设置,是否存在监控设置: ${!!globalSettings.monitorSettings},是否存在代理设置: ${!!globalSettings.agentSettings}`
);
+67 -115
View File
@@ -1,92 +1,59 @@
/**
* StatusService.ts
* 状态页服务,处理状态页配置、监控项和客户端相关的业务逻辑
*/
import * as repositories from "../repositories";
import { Bindings, Agent } from "../models";
import { Agent } from "../models";
/**
* 获取状态页配置(管理员)
* @param env 环境变量,包含数据库连接
* @param userId 用户ID
* @returns 状态页配置信息
*/
export async function getStatusPageConfig(
env: { DB: Bindings["DB"] },
userId: number
) {
export async function getStatusPageConfig(userId: number) {
try {
// 检查是否已存在配置
const existingConfig = await repositories.getStatusPageConfigById(
env.DB,
userId
);
const existingConfig = await repositories.getStatusPageConfigById(userId);
let configId: number;
if (existingConfig && existingConfig.id) {
configId = existingConfig.id;
} else {
// 创建默认配置
const insertResult = await repositories.createStatusPageConfig(
env.DB,
userId,
"系统状态",
"实时监控系统状态",
"",
""
);
if (!insertResult || typeof insertResult.id !== "number") {
throw new Error("创建状态页配置失败");
}
configId = insertResult.id;
if (!existingConfig) {
throw new Error("状态页配置不存在");
}
// 获取该用户的所有监控项
console.log("existingConfig", existingConfig);
// 获取被选中的监控项
const monitorsResult = await repositories.getConfigMonitors(
env.DB,
configId,
userId
existingConfig.id
);
// 获取该用户的所有客户端
// 获取所有监控项
const allMonitors = await repositories.getAllMonitors();
console.log("monitorsResult", monitorsResult);
// 获取被选中的客户端
const agentsResult = await repositories.getConfigAgents(
env.DB,
configId,
userId
existingConfig.id
);
// 构建配置对象返回
const config = await env.DB.prepare(
"SELECT * FROM status_page_config WHERE id = ?"
)
.bind(configId)
.first<{
title?: string;
description?: string;
logo_url?: string;
custom_css?: string;
}>();
// 获取所有客户端
const allAgents = await repositories.getAllAgents();
console.log("agentsResult", agentsResult);
// 构建返回的监控列表,将 allMonitors 和 monitorsResult 去重,同时 monitorsResult 中存在的需要标记为选中
const monitors = allMonitors.map((monitor: any) => {
const isSelected = monitorsResult.some(
(m: any) => m.monitor_id === monitor.id
);
return { ...monitor, selected: isSelected };
});
// 构建返回的客户端列表,将 allAgents 和 agentsResult 去重,同时 agentsResult 中存在的需要标记为选中
const agents = allAgents.map((agent: any) => {
const isSelected = agentsResult.some(
(a: any) => a.agent_id === agent.id
);
return { ...agent, selected: isSelected };
});
return {
title: config?.title || "",
description: config?.description || "",
logoUrl: config?.logo_url || "",
customCss: config?.custom_css || "",
monitors:
monitorsResult.results?.map((m) => ({
id: m.id,
name: m.name,
selected: m.selected === 1,
})) || [],
agents:
agentsResult.results?.map((a) => ({
id: a.id,
name: a.name,
selected: a.selected === 1,
})) || [],
title: existingConfig?.title || "",
description: existingConfig?.description || "",
logoUrl: existingConfig?.logo_url || "",
customCss: existingConfig?.custom_css || "",
monitors: monitors,
agents: agents,
};
} catch (error) {
console.error("获取状态页配置失败:", error);
@@ -94,15 +61,7 @@ export async function getStatusPageConfig(
}
}
/**
* 保存状态页配置
* @param env 环境变量,包含数据库连接
* @param userId 用户ID
* @param data 要保存的配置数据
* @returns 保存结果
*/
export async function saveStatusPageConfig(
env: { DB: Bindings["DB"] },
userId: number,
data: {
title: string;
@@ -115,17 +74,13 @@ export async function saveStatusPageConfig(
) {
try {
// 检查是否已存在配置
const existingConfig = await repositories.getStatusPageConfigById(
env.DB,
userId
);
const existingConfig = await repositories.getStatusPageConfigById(userId);
let configId: number;
if (existingConfig && existingConfig.id) {
// 更新现有配置
await repositories.updateStatusPageConfig(
env.DB,
existingConfig.id,
data.title,
data.description,
@@ -137,7 +92,6 @@ export async function saveStatusPageConfig(
} else {
// 创建新配置
const insertResult = await repositories.createStatusPageConfig(
env.DB,
userId,
data.title,
data.description,
@@ -153,42 +107,39 @@ export async function saveStatusPageConfig(
}
// 清除现有的监控项关联
await repositories.clearConfigMonitorLinks(env.DB, configId);
await repositories.clearConfigMonitorLinks(configId);
// 清除现有的客户端关联
await repositories.clearConfigAgentLinks(env.DB, configId);
await repositories.clearConfigAgentLinks(configId);
// 添加选定的监控项
if (data.monitors && data.monitors.length > 0) {
for (const monitorId of data.monitors) {
await repositories.addMonitorToConfig(env.DB, configId, monitorId);
await repositories.addMonitorToConfig(configId, monitorId);
}
}
// 添加选定的客户端
if (data.agents && data.agents.length > 0) {
for (const agentId of data.agents) {
await repositories.addAgentToConfig(env.DB, configId, agentId);
await repositories.addAgentToConfig(configId, agentId);
}
}
return { success: true, configId };
return existingConfig;
} catch (error) {
console.error("保存状态页配置失败:", error);
throw new Error("保存状态页配置失败");
}
}
/**
* 获取状态页公开数据
* @param env 环境变量,包含数据库连接
* @returns 状态页公开数据
*/
export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
export async function getStatusPagePublicData() {
// 获取所有配置
const configsResult = await repositories.getAllStatusPageConfigs(env.DB);
const configsResult = await repositories.getAllStatusPageConfigs();
if (!configsResult.results || configsResult.results.length === 0) {
console.log("configsResult", configsResult);
if (!configsResult || configsResult.length === 0) {
console.log("没有找到任何配置");
return {
@@ -202,33 +153,33 @@ export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
}
// 简单处理:获取第一个配置
const config = configsResult.results[0];
const config = configsResult[0];
// 获取选中的监控项
const selectedMonitors = await repositories.getSelectedMonitors(
env.DB,
config.id as number
);
console.log("selectedMonitors", selectedMonitors);
// 获取选中的客户端
const selectedAgents = await repositories.getSelectedAgents(
env.DB,
config.id as number
);
console.log("selectedAgents", selectedAgents);
// 获取监控项详细信息
let monitors: any[] = [];
if (selectedMonitors.results && selectedMonitors.results.length > 0) {
const monitorIds = selectedMonitors.results.map((m) => m.monitor_id);
if (selectedMonitors && selectedMonitors.length > 0) {
const monitorIds = selectedMonitors.map((m: any) => m.monitor_id);
if (monitorIds.length > 0) {
for (const monitorId of monitorIds) {
const monitor = await repositories.getMonitorById(env.DB, monitorId);
const monitor = await repositories.getMonitorById(monitorId);
const monitorDailyStats = await repositories.getMonitorDailyStatsById(
env.DB,
monitorId
);
const monitorHistory = await repositories.getMonitorStatusHistoryIn24h(
env.DB,
monitorId
);
monitors.push({
@@ -240,16 +191,17 @@ export async function getStatusPagePublicData(env: { DB: Bindings["DB"] }) {
}
}
// 获取客户端基本信息
let agents: Agent[] = [];
if (selectedAgents.results && selectedAgents.results.length > 0) {
const agentIds = selectedAgents.results.map((a) => a.agent_id);
// 获取客户端基本信息
if (selectedAgents && selectedAgents.length > 0) {
const agentIds = selectedAgents.map((a: any) => a.agent_id);
if (agentIds.length > 0) {
const agentsResult = await repositories.getAgentsByIds(env.DB, agentIds);
const agentsResult = await repositories.getAgentsByIds(agentIds);
if (agentsResult.results) {
agents = agentsResult.results;
if (agentsResult) {
agents = agentsResult as Agent[];
}
}
}
+12 -79
View File
@@ -1,22 +1,7 @@
/**
* UserService.ts
* 用户服务,处理用户管理相关的业务逻辑
*/
import { compare, hash } from "bcryptjs";
import { Bindings } from "../models/db";
import * as repositories from "../repositories";
/**
* 获取所有用户
* @param env 环境变量,包含数据库连接
* @param userRole 当前用户角色
* @returns 用户列表或错误消息
*/
export async function getAllUsersService(
env: { DB: Bindings["DB"] },
userRole: string
) {
export async function getAllUsersService(userRole: string) {
try {
// 检查权限
if (userRole !== "admin") {
@@ -24,25 +9,16 @@ export async function getAllUsersService(
}
// 查询所有用户,不包括密码
const result = await repositories.getAllUsers(env.DB);
const result = await repositories.getAllUsers();
return { success: true, users: result.results, status: 200 };
return { success: true, users: result, status: 200 };
} catch (error) {
console.error("获取用户列表错误:", error);
return { success: false, message: "获取用户列表失败", status: 500 };
}
}
/**
* 获取单个用户信息
* @param env 环境变量,包含数据库连接
* @param id 用户ID
* @param currentUserId 当前用户ID
* @param userRole 当前用户角色
* @returns 用户信息或错误消息
*/
export async function getUserByIdService(
env: { DB: Bindings["DB"] },
id: number,
currentUserId: number,
userRole: string
@@ -54,7 +30,7 @@ export async function getUserByIdService(
}
// 查询用户,不包括密码
const user = await repositories.getUserById(env.DB, id);
const user = await repositories.getUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
@@ -67,15 +43,7 @@ export async function getUserByIdService(
}
}
/**
* 创建用户
* @param env 环境变量,包含数据库连接
* @param userData 用户数据
* @param userRole 当前用户角色
* @returns 创建结果
*/
export async function createUserService(
env: { DB: Bindings["DB"] },
userData: {
username: string;
password: string;
@@ -96,10 +64,7 @@ export async function createUserService(
}
// 检查用户名是否已存在
const existingUser = await repositories.checkUserExists(
env.DB,
userData.username
);
const existingUser = await repositories.checkUserExists(userData.username);
if (existingUser) {
return { success: false, message: "用户名已存在", status: 400 };
@@ -116,7 +81,6 @@ export async function createUserService(
// 插入新用户
const newUser = await repositories.createUser(
env.DB,
userData.username,
hashedPassword,
userData.email || null,
@@ -130,17 +94,7 @@ export async function createUserService(
}
}
/**
* 更新用户信息
* @param env 环境变量,包含数据库连接
* @param id 用户ID
* @param updateData 更新数据
* @param currentUserId 当前用户ID
* @param userRole 当前用户角色
* @returns 更新结果
*/
export async function updateUserService(
env: { DB: Bindings["DB"] },
id: number,
updateData: {
username?: string;
@@ -158,7 +112,7 @@ export async function updateUserService(
}
// 检查用户是否存在
const user = await repositories.getFullUserById(env.DB, id);
const user = await repositories.getFullUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
@@ -182,9 +136,7 @@ export async function updateUserService(
) {
// 检查新用户名是否已存在
const existingUser = await repositories.checkUserExists(
env.DB,
updateData.username,
id
updateData.username
);
if (existingUser) {
@@ -209,7 +161,7 @@ export async function updateUserService(
// 执行更新
try {
const updatedUser = await repositories.updateUser(env.DB, id, updates);
const updatedUser = await repositories.updateUser(id, updates);
return { success: true, user: updatedUser, status: 200 };
} catch (err) {
if (err instanceof Error && err.message === "没有提供要更新的字段") {
@@ -223,16 +175,7 @@ export async function updateUserService(
}
}
/**
* 删除用户
* @param env 环境变量,包含数据库连接
* @param id 用户ID
* @param currentUserId 当前用户ID
* @param userRole 当前用户角色
* @returns 删除结果
*/
export async function deleteUserService(
env: { DB: Bindings["DB"] },
id: number,
currentUserId: number,
userRole: string
@@ -249,14 +192,14 @@ export async function deleteUserService(
}
// 检查用户是否存在
const user = await repositories.getUserById(env.DB, id);
const user = await repositories.getUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
}
// 执行删除
await repositories.deleteUser(env.DB, id);
await repositories.deleteUser(id);
return { success: true, message: "用户已删除", status: 200 };
} catch (error) {
@@ -265,17 +208,7 @@ export async function deleteUserService(
}
}
/**
* 修改用户密码
* @param env 环境变量,包含数据库连接
* @param id 用户ID
* @param passwordData 密码数据
* @param currentUserId 当前用户ID
* @param userRole 当前用户角色
* @returns 修改结果
*/
export async function changePasswordService(
env: { DB: Bindings["DB"] },
id: number,
passwordData: {
currentPassword?: string;
@@ -300,7 +233,7 @@ export async function changePasswordService(
}
// 获取用户
const user = await repositories.getFullUserById(env.DB, id);
const user = await repositories.getFullUserById(id);
if (!user) {
return { success: false, message: "用户不存在", status: 404 };
@@ -321,7 +254,7 @@ export async function changePasswordService(
const hashedPassword = await hash(passwordData.newPassword, 10);
// 更新密码
await repositories.updateUserPassword(env.DB, id, hashedPassword);
await repositories.updateUserPassword(id, hashedPassword);
return { success: true, message: "密码已更新", status: 200 };
} catch (error) {
-1773
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -35,8 +35,8 @@ const Dashboard = () => {
setLoading(true);
const [dashboardResponse] = await Promise.all([getDashboardData()]);
if (dashboardResponse) {
setMonitors(dashboardResponse.monitors);
setAgents(dashboardResponse.agents);
setMonitors(dashboardResponse.monitors || []);
setAgents(dashboardResponse.agents || []);
}
setLoading(false);
};
@@ -345,9 +345,8 @@ const CreateMonitor = () => {
<Button variant="secondary" onClick={() => navigate("/monitors")}>
{t("monitor.form.cancel")}
</Button>
<Button type="submit" disabled={loading}>
<Button type="submit" disabled={loading} onClick={handleSubmit}>
{loading ? t("monitor.form.creating") : t("monitor.form.create")}
{!loading && <PlusIcon />}
</Button>
</Flex>
</form>
+4 -1
View File
@@ -404,7 +404,10 @@ const EditMonitor = () => {
variant="soft"
color="red"
size="1"
onClick={() => removeHeader(index)}
onClick={(e) => {
e.preventDefault();
removeHeader(index);
}}
>
<TrashIcon />
</IconButton>
@@ -178,7 +178,7 @@ const MonitorDetail = () => {
</Button>
</Flex>
</Flex>
<Box pt="4" className="detail-content">
<Box pt="4" >
<Card>
<Flex direction="column" gap="2" className="ml-4">
<Heading size="4">{t("monitor.detailInfo")}</Heading>
@@ -198,7 +198,7 @@ const MonitorDetail = () => {
<Text>{t("monitor.expectedStatus")}:</Text>
<Text>{formatStatusCode(monitor.expected_status)}</Text>
<Text>{t("monitor.createTime")}:</Text>
<Text>{new Date(monitor.created_at).toLocaleString()}</Text>
<Text>{monitor.created_at}</Text>
<Text>{t("monitor.headers")}:</Text>
<Text style={{ overflowWrap: "break-word" }}>
{typeof monitor.headers === "string"
@@ -621,7 +621,11 @@ const NotificationsConfig = () => {
<Box p="2">
<Flex className="justify-between items-center mb-2">
<Text className="text-lg">{t("notifications.channels.title")}</Text>
<Button className="ml-auto" variant="secondary" onClick={handleAddChannelClick}>
<Button
className="ml-auto"
variant="secondary"
onClick={handleAddChannelClick}
>
<PlusIcon width="16" height="16" />
{t("notifications.channels.add")}
</Button>
@@ -662,7 +666,9 @@ const NotificationsConfig = () => {
return false;
}
})() && (
<Text className="text-xs">{t("common.default")}</Text>
<Text className="text-xs">
{t("common.default")}
</Text>
)}
</Flex>
<Text className="text-xs text-gray-600">
@@ -697,7 +703,7 @@ const NotificationsConfig = () => {
{t("common.edit")}
</Button>
<Button
className="ml-auto"
className="ml-auto"
variant="secondary"
onClick={() => handleDeleteChannelClick(channel.id)}
>
@@ -821,7 +827,7 @@ const NotificationsConfig = () => {
<Box p="1">
<Flex direction="column" gap="1">
<Flex justify="between" align="center">
<Box >
<Box>
<Text className="text-base">
{t("notifications.settings.monitors")}
</Text>
@@ -1517,8 +1523,8 @@ const NotificationsConfig = () => {
console.log(`[通知渠道] 选择类型: ${value}`); // 调试信息
}}
>
<SelectTrigger >
<SelectValue/>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="telegram">
@@ -1685,9 +1691,7 @@ const NotificationsConfig = () => {
</Flex>
<Flex gap="3" mt="6" justify="end">
<DialogClose>
{t("common.cancel")}
</DialogClose>
<DialogClose>{t("common.cancel")}</DialogClose>
<Button
className="ml-auto"
variant="secondary"
@@ -1720,9 +1724,7 @@ const NotificationsConfig = () => {
</DialogDescription>
<Flex gap="3" mt="6" justify="end">
<DialogClose>
{t("common.cancel")}
</DialogClose>
<DialogClose>{t("common.cancel")}</DialogClose>
<Button
color="red"
onClick={handleConfirmDeleteChannel}
@@ -1739,82 +1741,75 @@ const NotificationsConfig = () => {
return (
<Box>
<Container>
<Box mb="2">
<Flex
className="flex justify-between items-center detail-header"
>
<Flex align="center" gap="2">
<BellIcon width="20" height="20" />
<Heading size="5" weight="medium">
{t("notifications.title")}
</Heading>
</Flex>
<Button
className="ml-auto"
variant="secondary"
onClick={handleSave}
disabled={saving}
>
{saving ? t("common.savingChanges") : t("common.save")}
</Button>
<Box mb="2">
<Flex className="flex justify-between items-center detail-header">
<Flex align="center" gap="2">
<BellIcon width="20" height="20" />
<Heading size="5" weight="medium">
{t("notifications.title")}
</Heading>
</Flex>
<Text color="gray" size="2">
{t("notifications.description")}
</Text>
</Box>
<Button
className="ml-auto"
variant="secondary"
onClick={handleSave}
disabled={saving}
>
{saving ? t("common.savingChanges") : t("common.save")}
</Button>
</Flex>
<Text color="gray" size="2">
{t("notifications.description")}
</Text>
</Box>
{loading ? (
<Text>{t("common.loading")}...</Text>
) : (
<Card>
<Tabs defaultValue="global">
<TabsList>
<TabsTrigger value="global">
{t("notifications.tabs.global")}
</TabsTrigger>
<TabsTrigger value="channels">
{t("notifications.tabs.channels")}
</TabsTrigger>
<TabsTrigger value="templates">
{t("notifications.tabs.templates")}
</TabsTrigger>
<TabsTrigger value="specificMonitors">
{t("notifications.tabs.specificMonitors")}
</TabsTrigger>
<TabsTrigger value="specificAgents">
{t("notifications.tabs.specificAgents")}
</TabsTrigger>
</TabsList>
{loading ? (
<Text>{t("common.loading")}...</Text>
) : (
<Card>
<Tabs defaultValue="global">
<TabsList>
<TabsTrigger value="global">
{t("notifications.tabs.global")}
</TabsTrigger>
<TabsTrigger value="channels">
{t("notifications.tabs.channels")}
</TabsTrigger>
<TabsTrigger value="templates">
{t("notifications.tabs.templates")}
</TabsTrigger>
<TabsTrigger value="specificMonitors">
{t("notifications.tabs.specificMonitors")}
</TabsTrigger>
<TabsTrigger value="specificAgents">
{t("notifications.tabs.specificAgents")}
</TabsTrigger>
</TabsList>
<Box pt="2" px="2" >
<TabsContent value="global">
{renderGlobalSettingsTab()}
</TabsContent>
<Box pt="2" px="2">
<TabsContent value="global">
{renderGlobalSettingsTab()}
</TabsContent>
<TabsContent value="channels" >
{renderChannelsTab()}
</TabsContent>
<TabsContent value="channels">
{renderChannelsTab()}
</TabsContent>
<TabsContent value="templates">
{renderTemplatesTab()}
</TabsContent>
<TabsContent value="templates">
{renderTemplatesTab()}
</TabsContent>
<TabsContent
value="specificMonitors"
>
{renderSpecificMonitorsTab()}
</TabsContent>
<TabsContent value="specificMonitors">
{renderSpecificMonitorsTab()}
</TabsContent>
<TabsContent value="specificAgents">
{renderSpecificAgentsTab()}
</TabsContent>
</Box>
</Tabs>
</Card>
)}
<TabsContent value="specificAgents">
{renderSpecificAgentsTab()}
</TabsContent>
</Box>
</Tabs>
</Card>
)}
</Container>
{/* 渠道管理对话框 */}
+116 -120
View File
@@ -106,138 +106,134 @@ const UserProfile = () => {
}
return (
<Box>
<div className="page-container detail-page">
<Flex justify="between" align="center" className="detail-header">
<Heading size="6">{t("profile.title")}</Heading>
</Flex>
<Box className="sm:px-6 lg:px-[8%]">
<Flex justify="between" align="center">
<Heading size="6">{t("profile.title")}</Heading>
</Flex>
<div className="detail-content">
<Flex direction="column" gap="6">
<Card>
<Heading size="4" mb="4">
{t("profile.basicInfo")}
</Heading>
<Flex direction="column" gap="6" className="mt-4 mb-4">
<Card>
<Heading size="4" mb="4" className="ml-4">
{t("profile.basicInfo")}
</Heading>
{profileError && (
<Text color="red" mb="3">
{profileError}
{profileError && (
<Text color="red" mb="3">
{profileError}
</Text>
)}
{profileSuccess && (
<Text color="green" mb="3">
{profileSuccess}
</Text>
)}
<form onSubmit={handleProfileUpdate}>
<Flex direction="column" gap="3" className="ml-4">
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("user.username")}
</Text>
)}
{profileSuccess && (
<Text color="green" mb="3">
{profileSuccess}
<TextField.Input
value={username}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUsername(e.target.value)
}
required
/>
</Flex>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("user.email")}
</Text>
)}
<TextField.Input
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
/>
</Flex>
<form onSubmit={handleProfileUpdate}>
<Flex direction="column" gap="3">
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("user.username")}
</Text>
<TextField.Input
value={username}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setUsername(e.target.value)
}
required
/>
</Flex>
<Button type="submit" disabled={isProfileLoading}>
{isProfileLoading
? t("common.savingChanges")
: t("profile.update")}
</Button>
</Flex>
</form>
</Card>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("user.email")}
</Text>
<TextField.Input
type="email"
value={email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
/>
</Flex>
<Card>
<Heading size="4" mb="4">
{t("profile.changePassword")}
</Heading>
<Button type="submit" disabled={isProfileLoading}>
{isProfileLoading
? t("common.savingChanges")
: t("profile.update")}
</Button>
</Flex>
</form>
</Card>
{passwordError && (
<Text color="red" mb="3">
{passwordError}
</Text>
)}
{passwordSuccess && (
<Text color="green" mb="3">
{passwordSuccess}
</Text>
)}
<Card>
<Heading size="4" mb="4">
{t("profile.changePassword")}
</Heading>
{passwordError && (
<Text color="red" mb="3">
{passwordError}
<form onSubmit={handlePasswordChange}>
<Flex direction="column" gap="3" className="ml-4">
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.currentPassword")}
</Text>
)}
{passwordSuccess && (
<Text color="green" mb="3">
{passwordSuccess}
<TextField.Input
type="password"
value={currentPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCurrentPassword(e.target.value)
}
required
/>
</Flex>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.newPassword")}
</Text>
)}
<TextField.Input
type="password"
value={newPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewPassword(e.target.value)
}
required
/>
</Flex>
<form onSubmit={handlePasswordChange}>
<Flex direction="column" gap="3">
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.currentPassword")}
</Text>
<TextField.Input
type="password"
value={currentPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCurrentPassword(e.target.value)
}
required
/>
</Flex>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.confirmNewPassword")}
</Text>
<TextField.Input
type="password"
value={confirmPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmPassword(e.target.value)
}
required
/>
</Flex>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.newPassword")}
</Text>
<TextField.Input
type="password"
value={newPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setNewPassword(e.target.value)
}
required
/>
</Flex>
<Flex direction="column" gap="1">
<Text size="2" weight="medium">
{t("profile.confirmNewPassword")}
</Text>
<TextField.Input
type="password"
value={confirmPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setConfirmPassword(e.target.value)
}
required
/>
</Flex>
<Button type="submit" disabled={isPasswordLoading}>
{isPasswordLoading
? t("common.savingChanges")
: t("profile.changePasswordButton")}
</Button>
</Flex>
</form>
</Card>
</Flex>
</div>
</div>
<Button type="submit" disabled={isPasswordLoading}>
{isPasswordLoading
? t("common.savingChanges")
: t("profile.changePasswordButton")}
</Button>
</Flex>
</form>
</Card>
</Flex>
</Box>
);
};
+2 -2
View File
@@ -61,10 +61,10 @@ const UsersList = () => {
};
return (
<Box p="4">
<Box className="sm:px-6 lg:px-[8%]">
<Flex justify="between" align="center" mb="4">
<Heading size="6">{t("users.title")}</Heading>
<Button onClick={() => navigate("/users/create")}>
<Button variant="secondary" onClick={() => navigate("/users/create")}>
{t("users.create")}
</Button>
</Flex>