From 0877ad013f3958f6edd07d436823dd2dae8e16ef Mon Sep 17 00:00:00 2001 From: zaunist Date: Wed, 25 Jun 2025 20:23:40 +0800 Subject: [PATCH] feat: and cf deploy button --- .gitignore | 3 + .trae/rules/project_rules.md | 137 +++ backend/drizzle/0002_public_domino.sql | 3 + backend/drizzle/meta/0002_snapshot.json | 1314 +++++++++++++++++++++++ backend/drizzle/meta/_journal.json | 7 + backend/src/db/generated-migrations.ts | 24 + backend/src/db/schema.ts | 16 +- backend/src/index.ts | 41 + backend/src/jobs/monitor-task.ts | 36 +- backend/src/middlewares/auth.ts | 6 + backend/src/models/db.ts | 6 + frontend/src/api/client.ts | 4 +- package.json | 20 + backend/wrangler.toml => wrangler.toml | 7 +- 14 files changed, 1602 insertions(+), 22 deletions(-) create mode 100644 .trae/rules/project_rules.md create mode 100644 backend/drizzle/0002_public_domino.sql create mode 100644 backend/drizzle/meta/0002_snapshot.json create mode 100644 package.json rename backend/wrangler.toml => wrangler.toml (66%) diff --git a/.gitignore b/.gitignore index 976bd51..340867e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ .pnp.js node_modules/ +.wrangler +xugou.wiki + # --- Project Specific: Agent --- # Build artifacts for the agent project /agent/xugou-agent diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..9dea35b --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,137 @@ +# XUGOU 项目开发规则 + +## 项目概述 + +XUGOU 是一个基于 CloudFlare 的轻量化系统监控平台,采用现代化的全栈架构: +- **Frontend**: React + TypeScript + Vite + TailwindCSS + Radix UI +- **Backend**: Hono + TypeScript + CloudFlare Workers + D1 Database +- **Agent**: Go + Cobra CLI + gopsutil +- **部署**: CloudFlare Workers + D1 + Pages + +## 代码规范 + +### 通用规范 + +1. **代码是写给人看的,只是机器恰好可以运行而已!** +2. **每次修改代码以后,需要确保代码能够编译通过!** +3. 使用 TypeScript 进行类型安全开发 +4. 遵循 ESLint 和 Prettier 配置 +5. 使用有意义的变量和函数命名 +6. 添加必要的注释,特别是复杂业务逻辑 +7. 保持代码简洁,避免过度工程化 + +### Frontend 规范 + +#### 文件组织 +``` +src/ +├── api/ # API 调用封装 +├── components/ # 可复用组件 +│ └── ui/ # 基础 UI 组件 (shadcn/ui) +├── config/ # 配置文件 +├── hooks/ # 自定义 React Hooks +├── i18n/ # 国际化配置 +├── lib/ # 工具库 +├── pages/ # 页面组件 +├── providers/ # Context Providers +├── router/ # 路由配置 +├── styles/ # 样式文件 +├── types/ # TypeScript 类型定义 +└── utils/ # 工具函数 +``` + +#### 组件规范 +1. 使用函数式组件和 React Hooks +2. 组件文件使用 PascalCase 命名 (如 `MonitorCard.tsx`) +3. 导出组件使用 default export +4. Props 接口命名为 `ComponentNameProps` +5. 使用 Radix UI 作为基础组件库 +6. 样式使用 TailwindCSS,避免内联样式 +7. 状态管理优先使用 React Context,复杂状态考虑 Zustand + +#### 示例组件结构 +```typescript +interface MonitorCardProps { + monitor: Monitor; + onEdit?: (monitor: Monitor) => void; +} + +export default function MonitorCard({ monitor, onEdit }: MonitorCardProps) { + // 组件逻辑 + return ( + + {/* 组件内容 */} + + ); +} +``` + +### Backend 规范 + +#### 文件组织 +``` +src/ +├── api/ # API 路由处理 +├── config/ # 配置文件 +├── db/ # 数据库相关 +│ └── schema.ts # Drizzle ORM 模式定义 +├── jobs/ # 定时任务 +├── middlewares/ # 中间件 +├── models/ # 数据模型和类型 +├── repositories/ # 数据访问层 +├── services/ # 业务逻辑层 +├── static/ # 静态资源 +└── utils/ # 工具函数 +``` + +#### API 规范 +1. 使用 Hono 框架构建 RESTful API +2. 路由文件按功能模块组织 (如 `auth.ts`, `monitors.ts`) +3. 统一的错误处理和响应格式 +4. 使用中间件处理 CORS、JWT 认证等 +5. API 路径统一使用 `/api/` 前缀 + +#### 数据库规范 +1. 使用 Drizzle ORM 进行数据库操作 +2. 所有表结构定义在 `schema.ts` 中 +3. 使用 SQLite (CloudFlare D1) 作为数据库 +4. 表名使用复数形式,字段使用 snake_case +5. 必须包含 `created_at` 和 `updated_at` 字段 +6. 使用 Repository 模式封装数据访问 + +#### 示例 API 结构 +```typescript +const monitors = new Hono<{ Bindings: Bindings }>(); + +monitors.get('/', async (c) => { + try { + const result = await MonitorService.getAllMonitors(c.env); + return c.json({ success: true, data: result }); + } catch (error) { + return c.json({ success: false, message: error.message }, 500); + } +}); +``` + +### Agent 规范 + +#### 文件组织 +``` +cmd/ +└── agent/ # CLI 命令定义 +pkg/ +├── collector/ # 数据收集器 +├── config/ # 配置管理 +├── model/ # 数据模型 +├── reporter/ # 数据上报 +└── utils/ # 工具函数 +``` + +#### Go 代码规范 +1. 遵循 Go 官方代码规范 +2. 使用 Cobra 构建 CLI 应用 +3. 使用 Viper 进行配置管理 +4. 错误处理要完整,不忽略任何错误 +5. 包名使用小写,避免下划线 +6. 导出的函数和类型使用 PascalCase +7. 私有函数和变量使用 camelCase diff --git a/backend/drizzle/0002_public_domino.sql b/backend/drizzle/0002_public_domino.sql new file mode 100644 index 0000000..2b6280f --- /dev/null +++ b/backend/drizzle/0002_public_domino.sql @@ -0,0 +1,3 @@ +CREATE INDEX `agent_metrics_24h_agent_timestamp_idx` ON `agent_metrics_24h` (`agent_id`,`timestamp`);--> statement-breakpoint +CREATE INDEX `monitor_daily_stats_monitor_id_date_idx` ON `monitor_daily_stats` (`monitor_id`,`date`);--> statement-breakpoint +CREATE INDEX `monitor_status_history_24h_monitor_timestamp_idx` ON `monitor_status_history_24h` (`monitor_id`,`timestamp`); \ No newline at end of file diff --git a/backend/drizzle/meta/0002_snapshot.json b/backend/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..0f637ce --- /dev/null +++ b/backend/drizzle/meta/0002_snapshot.json @@ -0,0 +1,1314 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8479041e-fe6e-4e45-ae21-645880bed63c", + "prevId": "c3674fd1-f5ba-4032-8da8-b96fc21d8666", + "tables": { + "agent_metrics_24h": { + "name": "agent_metrics_24h", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "agent_id": { + "name": "agent_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "cpu_usage": { + "name": "cpu_usage", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_cores": { + "name": "cpu_cores", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu_model": { + "name": "cpu_model", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_total": { + "name": "memory_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_used": { + "name": "memory_used", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_free": { + "name": "memory_free", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "memory_usage_rate": { + "name": "memory_usage_rate", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_1": { + "name": "load_1", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_5": { + "name": "load_5", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "load_15": { + "name": "load_15", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "disk_metrics": { + "name": "disk_metrics", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_metrics": { + "name": "network_metrics", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agent_metrics_24h_agent_timestamp_idx": { + "name": "agent_metrics_24h_agent_timestamp_idx", + "columns": [ + "agent_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "agent_metrics_24h_agent_id_agents_id_fk": { + "name": "agent_metrics_24h_agent_id_agents_id_fk", + "tableFrom": "agent_metrics_24h", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "agents": { + "name": "agents", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_addresses": { + "name": "ip_addresses", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "keepalive": { + "name": "keepalive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "agents_token_unique": { + "name": "agents_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "agents_created_by_users_id_fk": { + "name": "agents_created_by_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_daily_stats": { + "name": "monitor_daily_stats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "total_checks": { + "name": "total_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "up_checks": { + "name": "up_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "down_checks": { + "name": "down_checks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "avg_response_time": { + "name": "avg_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "min_response_time": { + "name": "min_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "max_response_time": { + "name": "max_response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "availability": { + "name": "availability", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "monitor_daily_stats_monitor_id_date_idx": { + "name": "monitor_daily_stats_monitor_id_date_idx", + "columns": [ + "monitor_id", + "date" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_daily_stats_monitor_id_monitors_id_fk": { + "name": "monitor_daily_stats_monitor_id_monitors_id_fk", + "tableFrom": "monitor_daily_stats", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitor_status_history_24h": { + "name": "monitor_status_history_24h", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "response_time": { + "name": "response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "monitor_status_history_24h_monitor_timestamp_idx": { + "name": "monitor_status_history_24h_monitor_timestamp_idx", + "columns": [ + "monitor_id", + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "monitor_status_history_24h_monitor_id_monitors_id_fk": { + "name": "monitor_status_history_24h_monitor_id_monitors_id_fk", + "tableFrom": "monitor_status_history_24h", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "monitors": { + "name": "monitors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval": { + "name": "interval", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expected_status": { + "name": "expected_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headers": { + "name": "headers", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "response_time": { + "name": "response_time", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "monitors_created_by_users_id_fk": { + "name": "monitors_created_by_users_id_fk", + "tableFrom": "monitors", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_channels": { + "name": "notification_channels", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_channels_created_by_users_id_fk": { + "name": "notification_channels_created_by_users_id_fk", + "tableFrom": "notification_channels", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_history": { + "name": "notification_history", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template_id": { + "name": "template_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sent_at": { + "name": "sent_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_history_channel_id_notification_channels_id_fk": { + "name": "notification_history_channel_id_notification_channels_id_fk", + "tableFrom": "notification_history", + "tableTo": "notification_channels", + "columnsFrom": [ + "channel_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notification_history_template_id_notification_templates_id_fk": { + "name": "notification_history_template_id_notification_templates_id_fk", + "tableFrom": "notification_history", + "tableTo": "notification_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_settings": { + "name": "notification_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'global'" + }, + "target_id": { + "name": "target_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_down": { + "name": "on_down", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_recovery": { + "name": "on_recovery", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_offline": { + "name": "on_offline", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "on_cpu_threshold": { + "name": "on_cpu_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cpu_threshold": { + "name": "cpu_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "on_memory_threshold": { + "name": "on_memory_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "memory_threshold": { + "name": "memory_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 85 + }, + "on_disk_threshold": { + "name": "on_disk_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "disk_threshold": { + "name": "disk_threshold", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 90 + }, + "channels": { + "name": "channels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_settings_user_id_users_id_fk": { + "name": "notification_settings_user_id_users_id_fk", + "tableFrom": "notification_settings", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_templates": { + "name": "notification_templates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "notification_templates_created_by_users_id_fk": { + "name": "notification_templates_created_by_users_id_fk", + "tableFrom": "notification_templates", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_agents": { + "name": "status_page_agents", + "columns": { + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "agent_id": { + "name": "agent_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_agents_config_id_status_page_config_id_fk": { + "name": "status_page_agents_config_id_status_page_config_id_fk", + "tableFrom": "status_page_agents", + "tableTo": "status_page_config", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_page_agents_agent_id_agents_id_fk": { + "name": "status_page_agents_agent_id_agents_id_fk", + "tableFrom": "status_page_agents", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_page_agents_config_id_agent_id_pk": { + "columns": [ + "config_id", + "agent_id" + ], + "name": "status_page_agents_config_id_agent_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_config": { + "name": "status_page_config", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'系统状态'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'系统当前运行状态'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'CURRENT_TIMESTAMP'" + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_config_user_id_users_id_fk": { + "name": "status_page_config_user_id_users_id_fk", + "tableFrom": "status_page_config", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "status_page_monitors": { + "name": "status_page_monitors", + "columns": { + "config_id": { + "name": "config_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monitor_id": { + "name": "monitor_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "status_page_monitors_config_id_status_page_config_id_fk": { + "name": "status_page_monitors_config_id_status_page_config_id_fk", + "tableFrom": "status_page_monitors", + "tableTo": "status_page_config", + "columnsFrom": [ + "config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "status_page_monitors_monitor_id_monitors_id_fk": { + "name": "status_page_monitors_monitor_id_monitors_id_fk", + "tableFrom": "status_page_monitors", + "tableTo": "monitors", + "columnsFrom": [ + "monitor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "status_page_monitors_config_id_monitor_id_pk": { + "columns": [ + "config_id", + "monitor_id" + ], + "name": "status_page_monitors_config_id_monitor_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json index ec97879..01d6ea2 100644 --- a/backend/drizzle/meta/_journal.json +++ b/backend/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1749736889133, "tag": "0001_fluffy_pestilence", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1750852918387, + "tag": "0002_public_domino", + "breakpoints": true } ] } \ No newline at end of file diff --git a/backend/src/db/generated-migrations.ts b/backend/src/db/generated-migrations.ts index 0cfadc7..e3d484e 100644 --- a/backend/src/db/generated-migrations.ts +++ b/backend/src/db/generated-migrations.ts @@ -198,5 +198,29 @@ CREATE TABLE IF NOT EXISTS \`users\` ( ); --> statement-breakpoint CREATE UNIQUE INDEX IF NOT EXISTS \`users_username_unique\` ON \`users\` (\`username\`);` + }, + { + name: "0001_fluffy_pestilence.sql", + sql: `ALTER TABLE \`agents\` DROP COLUMN \`cpu_usage\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`memory_total\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`memory_used\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`disk_total\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`disk_used\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`network_rx\`; +--> statement-breakpoint +ALTER TABLE \`agents\` DROP COLUMN \`network_tx\`;` + }, + { + name: "0002_public_domino.sql", + sql: `CREATE INDEX IF NOT EXISTS \`agent_metrics_24h_agent_timestamp_idx\` ON \`agent_metrics_24h\` (\`agent_id\`,\`timestamp\`); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS \`monitor_daily_stats_monitor_id_date_idx\` ON \`monitor_daily_stats\` (\`monitor_id\`,\`date\`); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS \`monitor_status_history_24h_monitor_timestamp_idx\` ON \`monitor_status_history_24h\` (\`monitor_id\`,\`timestamp\`);` } ]; diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index fe00624..0c7669e 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -1,4 +1,4 @@ -import { int, sqliteTable, text, real, primaryKey } from "drizzle-orm/sqlite-core"; +import { int, sqliteTable, text, real, primaryKey, index } from "drizzle-orm/sqlite-core"; // 用户表 export const users = sqliteTable("users", { @@ -40,7 +40,10 @@ export const monitorStatusHistory24h = sqliteTable("monitor_status_history_24h", response_time: int("response_time"), status_code: int("status_code"), error: text("error") -}); +}, (table) => ({ + // monitor_id 和 timestamp 的联合索引,用于优化按监控项和时间查询的性能 + monitorTimestampIdx: index("monitor_status_history_24h_monitor_timestamp_idx").on(table.monitor_id, table.timestamp) +})); // 监控每日统计表 export const monitorDailyStats = sqliteTable("monitor_daily_stats", { @@ -55,7 +58,9 @@ export const monitorDailyStats = sqliteTable("monitor_daily_stats", { max_response_time: int("max_response_time").default(0), availability: real("availability").default(0), created_at: text("created_at").notNull() -}); +}, (table) => ({ + monitorDateIdx: index("monitor_daily_stats_monitor_id_date_idx").on(table.monitor_id, table.date) +})); // 客户端表 export const agents = sqliteTable("agents", { @@ -90,7 +95,10 @@ export const agentMetrics24h = sqliteTable("agent_metrics_24h", { load_15: real("load_15"), disk_metrics: text("disk_metrics"), network_metrics: text("network_metrics") -}); +}, (table) => ({ + // agent_id 和 timestamp 的联合索引,用于优化按代理和时间查询的性能 + agentTimestampIdx: index("agent_metrics_24h_agent_timestamp_idx").on(table.agent_id, table.timestamp) +})); // 状态页配置表 export const statusPageConfig = sqliteTable("status_page_config", { diff --git a/backend/src/index.ts b/backend/src/index.ts index 3052710..1eab24b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -29,6 +29,47 @@ app.route("/api/users", api.users); app.route("/api/status", api.status); app.route("/api/notifications", api.notifications); app.route("/api/dashboard", api.dashboard); + +// 静态文件路由 - 处理所有非 API 请求,返回前端应用 +app.get("*", async (c) => { + const url = new URL(c.req.url); + + // 如果是 API 路由,跳过静态文件处理 + if (url.pathname.startsWith("/api/")) { + return c.notFound(); + } + + try { + // 尝试获取请求的静态文件 + const asset = await c.env.ASSETS.fetch(c.req.url); + + // 如果文件存在,直接返回 + if (asset.status === 200) { + return asset; + } + + // 如果文件不存在,返回 index.html 以支持 React Router + const indexUrl = new URL("/index.html", c.req.url); + const indexAsset = await c.env.ASSETS.fetch(indexUrl.toString()); + + if (indexAsset.status === 200) { + // 设置正确的 Content-Type + const response = new Response(indexAsset.body, { + status: 200, + headers: { + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-cache" + } + }); + return response; + } + + return c.notFound(); + } catch (error) { + console.error("静态文件服务错误:", error); + return c.text("Internal Server Error", 500); + } +}); // 数据库状态标志,用于记录数据库初始化状态 let dbInitialized = false; diff --git a/backend/src/jobs/monitor-task.ts b/backend/src/jobs/monitor-task.ts index 190b2cc..adf27b7 100644 --- a/backend/src/jobs/monitor-task.ts +++ b/backend/src/jobs/monitor-task.ts @@ -4,7 +4,11 @@ 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 { + monitorDailyStats, + monitorStatusHistory24h, + monitors, +} from "../db/schema"; import { and, gte, lte } from "drizzle-orm"; const monitorTask = new Hono<{ Bindings: Bindings }>(); @@ -25,7 +29,7 @@ async function checkMonitors(c: any) { // 检查每个监控 const results = await Promise.all( - monitors.map(async (monitor: Monitor) => { + monitors.map(async (monitor: Monitor) => { console.log(`开始检查监控: ${monitor.name} (ID: ${monitor.id})`); const checkResult = await checkMonitor(monitor); // 处理通知 @@ -166,12 +170,12 @@ async function generateDailyStats(c: any) { // 一次性获取所有监控 const monitorsResult = await db.select().from(monitors); - if (!monitorsResult.results || monitorsResult.results.length === 0) { + if (!monitorsResult || monitorsResult.length === 0) { console.log("没有找到监控"); return { success: true, message: "没有监控", processed: 0 }; } - const allMonitors = monitorsResult.results as Monitor[]; + const allMonitors = monitorsResult as Monitor[]; console.log(`找到 ${allMonitors.length} 个监控`); // 创建监控ID列表 @@ -191,14 +195,13 @@ async function generateDailyStats(c: any) { lte(monitorStatusHistory24h.timestamp, endTime) ) ); - - if (!historyResult.results || historyResult.results.length === 0) { + if (!historyResult || historyResult.length === 0) { console.log(`在 ${dateStr} 没有找到任何监控历史记录`); return { success: true, message: "没有历史记录", processed: 0 }; } - console.log(`找到 ${historyResult.results.length} 条历史记录`); + console.log(`找到 ${historyResult.length} 条历史记录`); // 按监控ID分组处理数据 const statsMap = new Map(); @@ -219,7 +222,7 @@ async function generateDailyStats(c: any) { } // 处理所有历史记录 - for (const record of historyResult.results) { + for (const record of historyResult) { const monitorId = record.monitor_id; if (!statsMap.has(monitorId)) continue; @@ -306,12 +309,14 @@ async function generateDailyStats(c: any) { // 从 24h 表中删除已处理的数据 console.log(`开始从24小时热表删除已处理的数据`); - await db.delete(monitorStatusHistory24h).where( - and( - gte(monitorStatusHistory24h.timestamp, startTime), - lte(monitorStatusHistory24h.timestamp, endTime) - ) - ); + await db + .delete(monitorStatusHistory24h) + .where( + and( + gte(monitorStatusHistory24h.timestamp, startTime), + lte(monitorStatusHistory24h.timestamp, endTime) + ) + ); console.log(`从24小时热表删除已处理的数据完成`); return { @@ -341,9 +346,10 @@ export default { const hour = now.getUTCHours(); const minute = now.getUTCMinutes(); - if (hour === 0 && minute === 5) { + if (hour == 0 && minute == 5) { // 生成每日监控统计数据 const statsResult = await generateDailyStats(c); + console.log("生成每日监控统计测试"); if (statsResult.error) { console.error("生成每日监控统计数据时出错:", statsResult.error); } diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 27a15b6..4f058e2 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -7,6 +7,12 @@ import { getJwtSecret } from "../utils/jwt"; * 验证请求中的JWT令牌并将解码的payload存入上下文 */ export const jwtMiddleware = async (c: Context, next: Next) => { + // 跳过所有非 API 路径的认证检查(用于静态文件服务) + if (!c.req.path.startsWith("/api/")) { + return next(); + } + + // 跳过特定的 API 端点 if ( (c.req.path.endsWith("/status") || c.req.path.endsWith("/register") || diff --git a/backend/src/models/db.ts b/backend/src/models/db.ts index 0dced24..b51fe4d 100644 --- a/backend/src/models/db.ts +++ b/backend/src/models/db.ts @@ -14,6 +14,11 @@ export interface D1PreparedStatement { raw(): Promise; } +// Cloudflare Workers Fetcher 类型定义 +export interface Fetcher { + fetch(input: RequestInfo, init?: RequestInit): Promise; +} + // 扩展D1Result meta属性的类型 export interface D1Meta { last_row_id?: number; @@ -39,4 +44,5 @@ export interface VersionMetadata { export type Bindings = { DB: D1Database; CF_VERSION_METADATA?: VersionMetadata; + ASSETS: Fetcher; }; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 41918ee..556cbf0 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3,8 +3,8 @@ import { ENV_API_BASE_URL, ENV_API_TIMEOUT } from "../config"; // 创建 axios 实例 const api = axios.create({ - baseURL: ENV_API_BASE_URL, // 从配置中获取API基础URL - timeout: ENV_API_TIMEOUT, // 从配置中获取超时设置 + baseURL: ENV_API_BASE_URL || "", // 从配置中获取API基础URL + timeout: ENV_API_TIMEOUT || 100000, // 从配置中获取超时设置 headers: { "Content-Type": "application/json", }, diff --git a/package.json b/package.json new file mode 100644 index 0000000..863655e --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "xugou", + "version": "1.0.0", + "description": "Xugou monitoring system", + "private": true, + "workspaces": [ + "frontend", + "backend" + ], + "scripts": { + "dev:frontend": "npm run dev --workspace=frontend", + "dev:backend": "npm run dev --workspace=backend", + "build:frontend": "npm run build --workspace=frontend", + "build:backend": "npm run build --workspace=backend", + "db:generate": "npm run db:generate --workspace=backend", + "build": "npm run build:frontend && npm run build:backend", + "deploy": "wrangler deploy", + "preview": "npm run dev --workspace=backend" + } +} \ No newline at end of file diff --git a/backend/wrangler.toml b/wrangler.toml similarity index 66% rename from backend/wrangler.toml rename to wrangler.toml index 1432508..6c800a5 100644 --- a/backend/wrangler.toml +++ b/wrangler.toml @@ -1,5 +1,5 @@ name = "xugou-backend" -main = "src/index.ts" +main = "backend/src/index.ts" compatibility_date = "2025-03-10" compatibility_flags = ["nodejs_compat"] @@ -12,5 +12,10 @@ binding = "DB" database_name = "xugou_db" database_id = "32a2ca5f-3a77-4b08-965e-804612557880" +[assets] +binding = "ASSETS" +directory = "frontend/dist" +not_found_handling = "single-page-application" + [version_metadata] binding = "CF_VERSION_METADATA" \ No newline at end of file