├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_EN.md ├── agent ├── README.md ├── cmd │ └── agent │ │ ├── root.go │ │ ├── start.go │ │ └── version.go ├── go.mod ├── go.sum ├── main.go └── pkg │ ├── collector │ └── collector.go │ ├── config │ └── config.go │ ├── model │ ├── collector.go │ └── reporter.go │ ├── reporter │ └── reporter.go │ └── utils │ └── utils.go ├── backend ├── drizzle.config.ts ├── drizzle │ ├── 0000_romantic_next_avengers.sql │ └── meta │ │ ├── 0000_snapshot.json │ │ └── _journal.json ├── package-lock.json ├── package.json ├── scripts │ └── generate-migrations.ts ├── src │ ├── api │ │ ├── agents.ts │ │ ├── auth.ts │ │ ├── dashboard.ts │ │ ├── index.ts │ │ ├── monitors.ts │ │ ├── notifications.ts │ │ ├── status.ts │ │ └── users.ts │ ├── config │ │ ├── db.ts │ │ └── index.ts │ ├── db │ │ ├── generated-migrations.ts │ │ ├── index.ts │ │ ├── migration.ts │ │ ├── schema.ts │ │ └── seed.ts │ ├── index.ts │ ├── jobs │ │ ├── agent-task.ts │ │ ├── index.ts │ │ └── monitor-task.ts │ ├── middlewares │ │ ├── auth.ts │ │ ├── cors.ts │ │ └── index.ts │ ├── models │ │ ├── agent.ts │ │ ├── db.ts │ │ ├── index.ts │ │ ├── monitor.ts │ │ ├── notification.ts │ │ ├── status.ts │ │ └── user.ts │ ├── repositories │ │ ├── agent.ts │ │ ├── index.ts │ │ ├── monitor.ts │ │ ├── notification.ts │ │ ├── status.ts │ │ └── users.ts │ ├── services │ │ ├── AgentService.ts │ │ ├── AuthService.ts │ │ ├── DashboardService.ts │ │ ├── MonitorService.ts │ │ ├── NotificationService.ts │ │ ├── StatusService.ts │ │ ├── UserService.ts │ │ └── index.ts │ └── utils │ │ └── jwt.ts ├── tsconfig.json └── wrangler.toml ├── frontend ├── .env.local.example ├── components.json ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.svg │ ├── logo.svg │ ├── manifest.webmanifest │ ├── site.webmanifest │ └── sw.js ├── src │ ├── api │ │ ├── agents.ts │ │ ├── auth.ts │ │ ├── client.ts │ │ ├── dashboard.ts │ │ ├── index.ts │ │ ├── monitors.ts │ │ ├── notifications.ts │ │ ├── status.ts │ │ └── users.ts │ ├── components │ │ ├── AgentCard.tsx │ │ ├── AgentStatusBar.tsx │ │ ├── ChannelSelector.tsx │ │ ├── LanguageSelector.tsx │ │ ├── Layout.tsx │ │ ├── MetricsChart.tsx │ │ ├── MonitorCard.tsx │ │ ├── MonitorStatusBar.tsx │ │ ├── Navbar.tsx │ │ ├── ResponseTimeChart.tsx │ │ ├── StatusCodeSelect.tsx │ │ ├── StatusSummaryCard.tsx │ │ └── ui │ │ │ ├── alert-dialog.tsx │ │ │ ├── avatar.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── index.tsx │ │ │ ├── input.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ ├── table.tsx │ │ │ ├── tabs.tsx │ │ │ ├── textarea.tsx │ │ │ └── tooltip.tsx │ ├── config │ │ └── index.ts │ ├── hooks │ │ └── use-mobile.ts │ ├── i18n │ │ ├── config.ts │ │ ├── en-US.ts │ │ └── zh-CN.ts │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ ├── pages │ │ ├── Dashboard.tsx │ │ ├── Home.tsx │ │ ├── NotFound.tsx │ │ ├── agents │ │ │ ├── AgentDetail.tsx │ │ │ ├── AgentsList.tsx │ │ │ ├── CreateAgent.tsx │ │ │ └── EditAgent.tsx │ │ ├── auth │ │ │ ├── Login.tsx │ │ │ └── Register.tsx │ │ ├── monitors │ │ │ ├── CreateMonitor.tsx │ │ │ ├── EditMonitor.tsx │ │ │ ├── MonitorDetail.tsx │ │ │ └── MonitorsList.tsx │ │ ├── notifications │ │ │ └── NotificationsConfig.tsx │ │ ├── status │ │ │ ├── StatusPage.tsx │ │ │ └── StatusPageConfig.tsx │ │ └── users │ │ │ ├── UserProfile.tsx │ │ │ └── UsersList.tsx │ ├── providers │ │ ├── AuthProvider.tsx │ │ └── LanguageProvider.tsx │ ├── router │ │ └── index.tsx │ ├── styles │ │ └── global.css │ ├── types │ │ ├── agents.ts │ │ ├── auth.ts │ │ ├── components.ts │ │ ├── index.ts │ │ ├── language.ts │ │ ├── monitors.ts │ │ ├── notification.ts │ │ ├── routes.ts │ │ ├── status.ts │ │ └── users.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── install-agent.sh /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | if: startsWith(github.ref, 'refs/tags/') 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Generate Changelog 16 | id: changelog 17 | run: | 18 | # 获取当前标签 19 | CURRENT_TAG=${GITHUB_REF#refs/tags/} 20 | 21 | # 查找上一个标签(如果存在) 22 | PREVIOUS_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "") 23 | 24 | # 生成变更日志 25 | echo "generating changelog from $PREVIOUS_TAG to $CURRENT_TAG" 26 | echo "changelog<> $GITHUB_OUTPUT 27 | 28 | if [ -z "$PREVIOUS_TAG" ]; then 29 | # 如果没有上一个标签,则获取所有提交 30 | echo "## 完整变更日志" >> $GITHUB_OUTPUT 31 | echo "" >> $GITHUB_OUTPUT 32 | git log --pretty=format:"- %s (%h) by %an" >> $GITHUB_OUTPUT 33 | else 34 | # 获取自上一个标签以来的提交 35 | echo "## 自 $PREVIOUS_TAG 以来的变更" >> $GITHUB_OUTPUT 36 | echo "" >> $GITHUB_OUTPUT 37 | git log $PREVIOUS_TAG..$CURRENT_TAG --pretty=format:"- %s (%h) by %an" >> $GITHUB_OUTPUT 38 | fi 39 | 40 | echo "" >> $GITHUB_OUTPUT 41 | echo "EOF" >> $GITHUB_OUTPUT 42 | 43 | - name: Release 44 | uses: softprops/action-gh-release@v2 45 | with: 46 | token: ${{ secrets.TOKEN }} 47 | generate_release_notes: true 48 | tag_name: ${{ github.ref_name }} 49 | name: ${{ github.ref_name }} 50 | body: | 51 | ${{ github.event.head_commit.message }} 52 | 53 | ${{ steps.changelog.outputs.changelog }} 54 | 55 | draft: false 56 | prerelease: false 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # --- General Dependencies --- 2 | # Dependencies managed by package managers 3 | .pnp/ 4 | .pnp.js 5 | node_modules/ 6 | 7 | # --- Project Specific: Agent --- 8 | # Build artifacts for the agent project 9 | /agent/xugou-agent 10 | /agent/xugou-agent-linux 11 | 12 | # --- Project Specific: Backend --- 13 | # Build artifacts and dependencies for the backend project 14 | backend/build/ 15 | backend/dist/ 16 | backend/node_modules/ 17 | backend/out/ 18 | 19 | # --- Project Specific: Frontend --- 20 | # Build artifacts and dependencies for the frontend project 21 | frontend/build/ 22 | frontend/dist/ 23 | frontend/node_modules/ 24 | frontend/out/ 25 | 26 | # --- Project Specific: Mobile (Expo) --- 27 | # Build artifacts, dependencies and Expo files for the mobile project 28 | mobile/.expo/ 29 | mobile/build/ 30 | mobile/dist/ 31 | mobile/node_modules/ 32 | mobile/ios/ 33 | mobile/android/ 34 | mobile/build-* 35 | 36 | # --- General Build & Distribution Artifacts --- 37 | # Common build output directories and files 38 | build/ 39 | dist/ 40 | out/ 41 | web-build/ 42 | *.tsbuildinfo 43 | 44 | # --- Expo / Metro --- 45 | # Root Expo and Metro bundler specific files/directories 46 | .expo/ 47 | .metro-health-check* 48 | expo-env.d.ts 49 | 50 | # --- Go Specific --- 51 | # Go language build artifacts and workspace files 52 | *.dll 53 | *.dylib 54 | *.exe 55 | *.exe~ 56 | *.out 57 | *.so 58 | *.test 59 | go.work 60 | 61 | # --- Logs --- 62 | # Log files and debug logs from package managers 63 | logs/ 64 | *.log 65 | npm-debug.* 66 | pnpm-debug.log* 67 | yarn-debug.* 68 | yarn-error.* 69 | 70 | # --- Testing --- 71 | # Test coverage reports 72 | coverage/ 73 | 74 | # --- Environment Variables --- 75 | # Environment configuration files (DO NOT commit sensitive info!) 76 | .env 77 | .env*.local 78 | /backend/.env 79 | 80 | # --- IDE / Editor Configuration --- 81 | # Files and directories generated by IDEs and editors 82 | .idea/ 83 | .vscode/ 84 | *.njsproj 85 | *.ntvs* 86 | *.sln 87 | *.suo 88 | 89 | # --- Temporary, Backup & Cache Files --- 90 | # Temporary files, cache, editor backups, and tool-specific temp dirs 91 | .cache/ 92 | .temp/ 93 | */.wrangler/ 94 | *.bak 95 | *.sw? # Includes *.swp, *.swo 96 | *.temp 97 | *.tmp 98 | *~ # Editor backup files 99 | .DS_Store 100 | 101 | # --- OS Specific --- 102 | # Files generated by operating systems 103 | .DS_Store # macOS 104 | Thumbs.db # Windows 105 | 106 | # --- Secrets / Keys / Native --- 107 | # Sensitive files like keys, certificates, and native build files (Ensure these are not required in repo) 108 | *.jks 109 | *.key 110 | *.mobileprovision 111 | *.orig.* # Native file backups 112 | *.p12 113 | *.p8 114 | *.pem 115 | 116 | # --- Database Files --- 117 | # Local database files 118 | *.db 119 | *.sqlite 120 | *.sqlite3 121 | 122 | # --- Compressed Files --- 123 | # Archive files 124 | *.rar 125 | *.tar.gz 126 | *.zip 127 | 128 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 129 | 130 | /mobile/service-account-key.json 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 XUGOU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XUGOU - 基于CloudFlare搭建的轻量化监控平台 2 | 3 |
4 | 5 | ![XUGOU Logo](frontend/public/logo.svg) 6 | 7 | XUGOU 是一个基于 CloudFlare 的轻量化系统监控平台,提供系统监控和状态页面功能。 8 | 9 | [English](./README_EN.md) | 简体中文 10 | 11 |
12 | 13 | ## 📅 开发计划 14 | 15 | 目前已实现的主要功能: 16 | 17 | - ✅ 系统监控 - 客户端资源监控与数据上报 18 | - ✅ HTTP 监控 - API 接口健康检测 19 | - ✅ 数据可视化 - 实时数据展示与历史趋势 20 | - ✅ 状态页面 - 可定制的服务状态页面 21 | - ✅ 告警通知 - 异常事件通过多渠道通知(电子邮件、Telegram等) 22 | - ❌ 移动APP - 方便在手机查看监控状态(维护不过来,后面打算将pwa实现好,就这样吧) 23 | 24 | ## ✨ 核心特性 25 | 26 | - 🖥️ **系统监控** 27 | - 实时监控 CPU、内存、磁盘、网络等系统指标 28 | - 支持自定义监控间隔 29 | - 全平台支持(agent由go编写,理论上go能编译的平台都可以支持) 30 | 31 | - 🌐 **HTTP 监控** 32 | - 支持 HTTP/HTTPS 接口监控 33 | - 自定义请求方法、头部和请求体 34 | - 响应时间、状态码和内容检查 35 | 36 | - 📊 **数据可视化** 37 | - 实时数据图表展示 38 | - 自定义仪表盘 39 | 40 | - 🌍 **状态页面** 41 | - 自定义状态页面 42 | - 支持多监控项展示 43 | - 响应式设计 44 | 45 | ## 🏗️ 系统架构 46 | 47 | XUGOU 采用现代化的系统架构,包含以下组件: 48 | 49 | - **Agent**: 轻量级系统监控客户端 50 | - **Backend**: 基于 Hono 开发的后端服务,支持部署在 Cloudflare Workers 上 51 | - **Frontend**: 基于 React + TypeScript 的现代化前端界面 52 | 53 | ## 🚀 快速开始 54 | 55 | ### 部署指南 56 | 57 | 默认用户名:admin 默认密码: admin123 58 | 59 | [XUGOU wiki 部署指南](https://github.com/zaunist/xugou/wiki) 60 | 61 | ### 视频教程 62 | 63 | [![XUGOU 视频教程](https://img.youtube.com/vi/jisEpcqDego/0.jpg)](https://youtu.be/J7_xtsJIYiM) 64 | 65 | ## 常见问题 66 | 67 | [XUGOU wiki 常见问题](https://github.com/zaunist/xugou/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) 68 | 69 | ## ⭐ 支持一下作者 70 | 71 | - 给项目点个 Star,分享给您的朋友 72 | - 通过 Buy Me a Coffee 支持我的持续开发 73 | 74 |
75 | 76 | Buy Me A Coffee 77 | 78 |
79 | 80 | ## 🤝 贡献 81 | 82 | 欢迎所有形式的贡献,无论是新功能、bug 修复还是文档改进。 83 | 84 | ## 📄 开源协议 85 | 86 | 本项目采用 MIT 协议开源,详见 [LICENSE](./LICENSE) 文件。 87 | 88 | ## 🔥 Star History 89 | 90 | [![Star History Chart](https://api.star-history.com/svg?repos=zaunist/xugou&type=Date)](https://www.star-history.com/#zaunist/xugou&Date) 91 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # XUGOU - Lightweight Monitoring Platform Based on CloudFlare 2 | 3 |
4 | 5 | ![XUGOU Logo](frontend/public/logo.svg) 6 | 7 | XUGOU is a lightweight system monitoring platform based on CloudFlare, providing system monitoring and status page functionality. 8 | 9 | English | [简体中文](./README.md) 10 | 11 |
12 | 13 | ## 📅 Development Plan 14 | 15 | Currently implemented features: 16 | 17 | - ✅ System Monitoring - Client resource monitoring and data reporting 18 | - ✅ HTTP Monitoring - API endpoint health detection and analysis 19 | - ✅ Data Visualization - Real-time data display and historical trend analysis 20 | - ✅ Status Page - Customizable service status page 21 | - ✅ Alert Notifications - Anomaly event notifications through multiple channels (Email, Telegram, etc.) 22 | - ❌ Mobile APP - Convenient monitoring status check on mobile devices 23 | 24 | ## ✨ Core Features 25 | 26 | - 🖥️ **System Monitoring** 27 | - Real-time monitoring of CPU, memory, disk, network and other system metrics 28 | - Support for custom monitoring intervals 29 | - Cross-platform support (agent written in Go, supporting all platforms where Go can compile) 30 | 31 | - 🌐 **HTTP Monitoring** 32 | - Support for HTTP/HTTPS endpoint monitoring 33 | - Custom request methods, headers, and request bodies 34 | - Response time, status code and content validation 35 | 36 | - 📊 **Data Visualization** 37 | - Real-time data chart display 38 | - Custom dashboards 39 | 40 | - 🌍 **Status Page** 41 | - Customizable status page 42 | - Support for multiple monitoring items 43 | - Responsive design 44 | 45 | ## 🏗️ System Architecture 46 | 47 | XUGOU adopts a modern system architecture, including the following components: 48 | 49 | - **Agent**: Lightweight system monitoring client 50 | - **Backend**: Backend service based on Cloudflare Workers 51 | - **Frontend**: Modern frontend interface based on React + TypeScript 52 | 53 | ## 🚀 Quick Start 54 | 55 | ### Deployment Guide 56 | 57 | Default username: admin Default password: admin123 58 | 59 | [XUGOU wiki Deployment Guide](https://github.com/zaunist/xugou/wiki) 60 | 61 | ### Video Tutorial 62 | 63 | [![XUGOU Video Tutorial](https://img.youtube.com/vi/jisEpcqDego/0.jpg)](https://youtu.be/J7_xtsJIYiM) 64 | 65 | ## FAQ 66 | 67 | [XUGOU wiki FAQ](https://github.com/zaunist/xugou/wiki) 68 | 69 | ## ⭐ Support the Author 70 | 71 | Sponsor me in any way you can: 72 | 73 | - Star the project and share it with your friends 74 | - Buy me a coffee 75 | 76 |
77 | 78 | Buy Me A Coffee 79 | 80 |
81 | 82 | ## 🤝 Contribution 83 | 84 | All forms of contributions are welcome, whether it's new features, bug fixes, or documentation improvements. 85 | 86 | ## 📄 License 87 | 88 | This project is open-sourced under the MIT License. See the [LICENSE](./LICENSE) file for details. 89 | 90 | ## 🔥 Star History 91 | 92 | [![Star History Chart](https://api.star-history.com/svg?repos=zaunist/xugou&type=Date)](https://www.star-history.com/#zaunist/xugou&Date) 93 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Xugou Agent 2 | 3 | Xugou Agent 是一个系统监控客户端,用于收集系统信息并上报到监控服务器。它可以收集 CPU、内存、磁盘、网络等系统信息,并定期上报到指定的服务器。 4 | 5 | ## 功能特点 6 | 7 | - 收集系统基本信息(主机名、操作系统、平台等) 8 | - 监控 CPU 使用率和负载 9 | - 监控内存使用情况 10 | - 监控磁盘使用情况 11 | - 监控网络接口状态 12 | - 支持自定义收集间隔 13 | - 支持自定义监控硬盘设备和网络设备 14 | - 支持配置文件和环境变量配置 15 | 16 | ## 计划 17 | 18 | - 压缩二进制文件,最小化体积,我希望能够在更多的低功耗平台运行 19 | 20 | ## 安装 21 | 22 | ### 从源码构建 23 | 24 | ```bash 25 | git clone https://github.com/zaunist/xugou.git 26 | cd agent 27 | go build -o xugou-agent 28 | ``` 29 | 30 | ## 使用方法 31 | 32 | ### 基本命令 33 | 34 | ```bash 35 | # 显示帮助信息 36 | ./xugou-agent --help 37 | 38 | # 显示版本信息 39 | ./xugou-agent version 40 | 41 | # 启动客户端 42 | ./xugou-agent start 43 | ``` 44 | 45 | ### 配置选项 46 | 47 | > 一般来说,建议使用命令行参数的方式来使用,网页上会提供一键安装使用的脚本 48 | 49 | 可以通过命令行参数、配置文件或环境变量来配置 Xugou Agent: 50 | 51 | #### 命令行参数 52 | 53 | ```bash 54 | # 指定服务器地址 55 | ./xugou-agent --server https://monitor.example.com 56 | 57 | # 指定 API 令牌 58 | ./xugou-agent --token YOUR_API_TOKEN 59 | 60 | # 指定收集间隔(秒) 61 | ./xugou-agent --interval 60 62 | 63 | # 指定http 代理 64 | ./xugou-agent --proxy http://proxy.example.com:8080 65 | ``` 66 | 67 | #### 环境变量 68 | 69 | 所有配置选项也可以通过环境变量设置,环境变量名称格式为 `XUGOU_*`: 70 | 71 | ```bash 72 | export XUGOU_SERVER=https://monitor.example.com 73 | export XUGOU_TOKEN=YOUR_API_TOKEN 74 | export XUGOU_INTERVAL=60 75 | export XUGOU_LOG_LEVEL=info 76 | ``` 77 | 78 | ## 开发 79 | 80 | ### 依赖项 81 | 82 | - Go 1.18 或更高版本 83 | - github.com/spf13/cobra 84 | - github.com/spf13/viper 85 | - github.com/shirou/gopsutil/v3 86 | 87 | ### 项目结构 88 | 89 | ``` 90 | agent/ 91 | ├── cmd/ 92 | │ └── agent/ # 命令行命令 93 | │ ├── root.go # 根命令 94 | │ ├── start.go # 启动命令 95 | │ └── version.go # 版本命令 96 | ├── pkg/ 97 | │ ├── collector/ # 数据收集器 98 | │ └── reporter/ # 数据上报器 99 | └── main.go # 程序入口 100 | ``` -------------------------------------------------------------------------------- /agent/cmd/agent/root.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var ( 13 | cfgFile string 14 | rootCmd = &cobra.Command{ 15 | Use: "xugou-agent", 16 | Short: "Xugou Agent - 系统监控客户端", 17 | Long: `Xugou Agent 是一个系统监控客户端,用于收集系统信息并上报到监控服务器。 18 | 它可以收集 CPU、内存、磁盘、网络等系统信息,并定期上报到指定的服务器。`, 19 | } 20 | ) 21 | 22 | // Execute 执行根命令 23 | func Execute() error { 24 | return rootCmd.Execute() 25 | } 26 | 27 | func init() { 28 | cobra.OnInitialize(initConfig) 29 | 30 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "配置文件路径 (默认为 $HOME/.xugou-agent.yaml)") 31 | rootCmd.PersistentFlags().String("server", "", "监控服务器地址(例如:https://api.xugou.mdzz.uk)") 32 | rootCmd.PersistentFlags().String("token", "", "API 令牌(例如: xugou_maxln220_df8900585981ab775b36dcaaaee772d8.f668c0cf84d1840d)") 33 | rootCmd.PersistentFlags().StringSlice("devices", []string{}, "指定监控的硬盘设备列表 (例如: /dev/sda1,/dev/sdb1)") 34 | rootCmd.PersistentFlags().StringSlice("interfaces", []string{}, "指定监控的网络接口列表 (例如: eth0,wlan0)") 35 | rootCmd.PersistentFlags().IntP("interval", "i", 60, "数据采集和上报间隔(秒)") 36 | rootCmd.PersistentFlags().StringP("proxy", "p", "", "HTTP代理服务器地址(例如:http://proxy.example.com:8080)") 37 | 38 | viper.BindPFlag("interval", rootCmd.PersistentFlags().Lookup("interval")) 39 | viper.BindPFlag("proxy", rootCmd.PersistentFlags().Lookup("proxy")) 40 | viper.BindPFlag("server", rootCmd.PersistentFlags().Lookup("server")) 41 | viper.BindPFlag("token", rootCmd.PersistentFlags().Lookup("token")) 42 | viper.BindPFlag("devices", rootCmd.PersistentFlags().Lookup("devices")) 43 | viper.BindPFlag("interfaces", rootCmd.PersistentFlags().Lookup("interfaces")) 44 | } 45 | 46 | func initConfig() { 47 | if cfgFile != "" { 48 | // 使用指定的配置文件 49 | viper.SetConfigFile(cfgFile) 50 | } else { 51 | // 查找用户主目录 52 | home, err := os.UserHomeDir() 53 | if err != nil { 54 | fmt.Println("错误: 无法获取用户主目录:", err) 55 | os.Exit(1) 56 | } 57 | 58 | // 在主目录中查找 .xugou-agent.yaml 文件 59 | viper.AddConfigPath(home) 60 | viper.SetConfigName(".xugou-agent") 61 | viper.SetConfigType("yaml") 62 | cfgFile = filepath.Join(home, ".xugou-agent.yaml") 63 | } 64 | 65 | // 读取环境变量 66 | viper.AutomaticEnv() 67 | viper.SetEnvPrefix("XUGOU") 68 | 69 | // 如果找到配置文件,则读取它 70 | if err := viper.ReadInConfig(); err == nil { 71 | fmt.Println("使用配置文件:", viper.ConfigFileUsed()) 72 | } else { 73 | if _, ok := err.(viper.ConfigFileNotFoundError); !ok { 74 | fmt.Println("警告: 配置文件读取错误:", err) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /agent/cmd/agent/start.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | "github.com/xugou/agent/pkg/collector" 14 | "github.com/xugou/agent/pkg/config" 15 | "github.com/xugou/agent/pkg/reporter" 16 | ) 17 | 18 | func init() { 19 | startCmd := &cobra.Command{ 20 | Use: "start", 21 | Short: "启动 Xugou Agent", 22 | Long: `启动 Xugou Agent 开始采集系统信息并上报到服务器`, 23 | Run: runStart, 24 | } 25 | rootCmd.AddCommand(startCmd) 26 | } 27 | 28 | func runStart(cmd *cobra.Command, args []string) { 29 | 30 | config.ServerURL = viper.GetString("server") 31 | config.Token = viper.GetString("token") 32 | config.Interval = viper.GetInt("interval") 33 | config.ProxyURL = viper.GetString("proxy") 34 | // 检查必要的配置 35 | 36 | if config.Token == "" { 37 | fmt.Println("错误: 未设置 API 令牌,请使用 --token 参数或在配置文件中设置") 38 | os.Exit(1) 39 | } 40 | 41 | if config.ServerURL == "" { 42 | fmt.Println("错误: 未设置服务器地址,请使用 --server 参数或在配置文件中设置") 43 | os.Exit(1) 44 | } 45 | 46 | fmt.Println("Xugou Agent 启动中...") 47 | fmt.Printf("服务器地址: %s\n", config.ServerURL) 48 | fmt.Printf("上报数据间隔: %d秒\n", config.Interval) 49 | if config.ProxyURL != "" { 50 | fmt.Printf("使用代理服务器: %s\n", config.ProxyURL) 51 | } 52 | fmt.Println("使用令牌自动注册/上报数据") 53 | 54 | // 设置上下文,用于处理取消信号 55 | ctx, cancel := context.WithCancel(context.Background()) 56 | defer cancel() 57 | 58 | // 初始化数据收集器和上报器 59 | dataCollector := collector.NewCollector() 60 | dataReporter := reporter.NewReporter() 61 | fmt.Println("使用HTTP上报器") 62 | 63 | // 设置定时器,按指定间隔上报数据 64 | ticker := time.NewTicker(time.Duration(config.Interval) * time.Second) 65 | defer ticker.Stop() 66 | 67 | // 设置信号处理,用于优雅退出 68 | sigCh := make(chan os.Signal, 1) 69 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 70 | 71 | // 启动时立即执行一次收集和上报 72 | go collectAndReport(ctx, dataCollector, dataReporter) 73 | 74 | fmt.Println("Xugou Agent 已启动,按 Ctrl+C 停止") 75 | 76 | // 主循环 77 | for { 78 | select { 79 | case <-ticker.C: 80 | go collectAndReportBatch(ctx, dataCollector, dataReporter) 81 | case sig := <-sigCh: 82 | fmt.Printf("收到信号 %v,正在停止...\n", sig) 83 | return 84 | } 85 | } 86 | } 87 | 88 | // collectAndReport 收集并上报系统信息 89 | func collectAndReport(ctx context.Context, c collector.Collector, r reporter.Reporter) { 90 | // 收集系统信息 91 | info, err := c.Collect(ctx) 92 | if err != nil { 93 | fmt.Printf("采集系统信息失败: %v\n", err) 94 | return 95 | } 96 | 97 | // 上报系统信息 98 | err = r.Report(ctx, info) 99 | if err != nil { 100 | fmt.Printf("上报系统信息失败: %v\n", err) 101 | return 102 | } 103 | 104 | fmt.Printf("系统信息已收集并上报,时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) 105 | } 106 | 107 | func collectAndReportBatch(ctx context.Context, c collector.Collector, r reporter.Reporter) { 108 | infoList, err := c.CollectBatch(ctx) 109 | if err != nil { 110 | fmt.Printf("采集系统信息失败: %v\n", err) 111 | return 112 | } 113 | 114 | fmt.Printf("采集到 %d 条系统信息\n", len(infoList)) 115 | 116 | err = r.ReportBatch(ctx, infoList) 117 | if err != nil { 118 | fmt.Printf("上报系统信息失败: %v\n", err) 119 | return 120 | } 121 | fmt.Printf("系统信息已收集并上报,时间: %s\n", time.Now().Format("2006-01-02 15:04:05")) 122 | } 123 | -------------------------------------------------------------------------------- /agent/cmd/agent/version.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | // 版本信息,可以在编译时通过 -ldflags 设置 11 | Version = "0.1.0" 12 | GitCommit = "unknown" 13 | BuildDate = "unknown" 14 | ) 15 | 16 | func init() { 17 | versionCmd := &cobra.Command{ 18 | Use: "version", 19 | Short: "显示版本信息", 20 | Long: `显示 Xugou Agent 的版本信息、构建日期和 Git 提交哈希`, 21 | Run: runVersion, 22 | } 23 | 24 | rootCmd.AddCommand(versionCmd) 25 | } 26 | 27 | func runVersion(cmd *cobra.Command, args []string) { 28 | fmt.Println("Xugou Agent 版本信息:") 29 | fmt.Printf("版本: %s\n", Version) 30 | fmt.Printf("Git 提交: %s\n", GitCommit) 31 | fmt.Printf("构建日期: %s\n", BuildDate) 32 | } 33 | -------------------------------------------------------------------------------- /agent/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xugou/agent 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/shirou/gopsutil/v3 v3.24.5 7 | github.com/spf13/cobra v1.9.1 8 | github.com/spf13/viper v1.20.0 9 | gopkg.in/yaml.v3 v3.0.1 10 | ) 11 | 12 | require ( 13 | github.com/fsnotify/fsnotify v1.8.0 // indirect 14 | github.com/go-ole/go-ole v1.2.6 // indirect 15 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 16 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 17 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 18 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 19 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 20 | github.com/sagikazarmark/locafero v0.8.0 // indirect 21 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 22 | github.com/sourcegraph/conc v0.3.0 // indirect 23 | github.com/spf13/afero v1.14.0 // indirect 24 | github.com/spf13/cast v1.7.1 // indirect 25 | github.com/spf13/pflag v1.0.6 // indirect 26 | github.com/subosito/gotenv v1.6.0 // indirect 27 | github.com/tklauser/go-sysconf v0.3.12 // indirect 28 | github.com/tklauser/numcpus v0.6.1 // indirect 29 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 30 | go.uber.org/multierr v1.11.0 // indirect 31 | golang.org/x/sys v0.31.0 // indirect 32 | golang.org/x/text v0.23.0 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/xugou/agent/cmd/agent" 8 | ) 9 | 10 | func main() { 11 | if err := agent.Execute(); err != nil { 12 | fmt.Fprintln(os.Stderr, err) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /agent/pkg/collector/collector.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/spf13/viper" 10 | 11 | "github.com/shirou/gopsutil/v3/cpu" 12 | "github.com/shirou/gopsutil/v3/disk" 13 | "github.com/shirou/gopsutil/v3/host" 14 | "github.com/shirou/gopsutil/v3/load" 15 | "github.com/shirou/gopsutil/v3/mem" 16 | "github.com/shirou/gopsutil/v3/net" 17 | "github.com/xugou/agent/pkg/config" 18 | "github.com/xugou/agent/pkg/model" 19 | "github.com/xugou/agent/pkg/utils" 20 | ) 21 | 22 | // Collector 定义数据收集器接口 23 | type Collector interface { 24 | Collect(ctx context.Context) (*model.SystemInfo, error) 25 | CollectBatch(ctx context.Context) ([]*model.SystemInfo, error) // 批量采集一段时间内的系统信息 26 | } 27 | 28 | // DefaultCollector 是默认的数据收集器实现 29 | type DefaultCollector struct{} 30 | 31 | // NewCollector 创建一个新的数据收集器 32 | func NewCollector() Collector { 33 | return &DefaultCollector{} 34 | } 35 | 36 | // Collect 收集系统信息 37 | func (c *DefaultCollector) Collect(ctx context.Context) (*model.SystemInfo, error) { 38 | info := &model.SystemInfo{ 39 | Timestamp: time.Now(), 40 | Keepalive: config.Interval, 41 | } 42 | 43 | info.Token = config.Token 44 | 45 | // 获取主机信息 46 | hostInfo, err := host.Info() 47 | if err != nil { 48 | return nil, fmt.Errorf("获取主机信息失败: %w", err) 49 | } 50 | info.Hostname = hostInfo.Hostname 51 | info.Platform = hostInfo.Platform 52 | info.OS = hostInfo.OS 53 | // 设置操作系统版本,格式化为更有意义的信息 54 | info.Version = fmt.Sprintf("%s %s (%s)", hostInfo.Platform, hostInfo.PlatformVersion, hostInfo.KernelVersion) 55 | 56 | // 获取本地IP地址 57 | info.IPAddresses = utils.GetLocalIPs() 58 | 59 | // 获取CPU信息 60 | cpuPercent, err := cpu.Percent(time.Second, false) 61 | if err != nil { 62 | return nil, fmt.Errorf("获取CPU使用率失败: %w", err) 63 | } 64 | 65 | cpuInfo, err := cpu.Info() 66 | if err != nil { 67 | return nil, fmt.Errorf("获取CPU信息失败: %w", err) 68 | } 69 | 70 | var modelName string 71 | if len(cpuInfo) > 0 { 72 | modelName = cpuInfo[0].ModelName 73 | } 74 | 75 | info.CPUInfo = model.CPUInfo{ 76 | Usage: cpuPercent[0], 77 | Cores: runtime.NumCPU(), 78 | ModelName: modelName, 79 | } 80 | 81 | // 获取内存信息 82 | memInfo, err := mem.VirtualMemory() 83 | if err != nil { 84 | return nil, fmt.Errorf("获取内存信息失败: %w", err) 85 | } 86 | 87 | info.MemoryInfo = model.MemoryInfo{ 88 | Total: memInfo.Total, 89 | Used: memInfo.Used, 90 | Free: memInfo.Free, 91 | UsageRate: memInfo.UsedPercent, 92 | } 93 | 94 | // 获取磁盘信息 95 | configDevices := viper.GetStringSlice("devices") 96 | deviceSet := make(map[string]struct{}) 97 | for _, d := range configDevices { 98 | deviceSet[d] = struct{}{} 99 | } 100 | 101 | partitions, err := disk.Partitions(false) 102 | if err != nil { 103 | return nil, fmt.Errorf("获取磁盘分区信息失败: %w", err) 104 | } 105 | 106 | for _, partition := range partitions { 107 | // 如果指定了设备列表,并且当前分区不在列表中,则跳过 108 | if len(configDevices) > 0 { 109 | _, deviceMatch := deviceSet[partition.Device] 110 | _, mountpointMatch := deviceSet[partition.Mountpoint] 111 | if !deviceMatch && !mountpointMatch { 112 | continue 113 | } 114 | } 115 | 116 | usage, err := disk.Usage(partition.Mountpoint) 117 | if err != nil { 118 | // log.Printf("获取磁盘 %s 使用情况失败: %v", partition.Mountpoint, err) // 可选的日志记录 119 | continue 120 | } 121 | 122 | diskInfo := model.DiskInfo{ 123 | Device: partition.Device, 124 | MountPoint: partition.Mountpoint, 125 | Total: usage.Total, 126 | Used: usage.Used, 127 | Free: usage.Free, 128 | UsageRate: usage.UsedPercent, 129 | FSType: partition.Fstype, 130 | } 131 | info.DiskInfo = append(info.DiskInfo, diskInfo) 132 | } 133 | 134 | // 获取网络信息 135 | configInterfaces := viper.GetStringSlice("interfaces") 136 | interfaceSet := make(map[string]struct{}) 137 | for _, i := range configInterfaces { 138 | interfaceSet[i] = struct{}{} 139 | } 140 | 141 | netIOCounters, err := net.IOCounters(true) 142 | if err != nil { 143 | return nil, fmt.Errorf("获取网络信息失败: %w", err) 144 | } 145 | 146 | for _, netIO := range netIOCounters { 147 | // 如果指定了接口列表,并且当前接口不在列表中,则跳过 148 | if len(configInterfaces) > 0 { 149 | if _, ok := interfaceSet[netIO.Name]; !ok { 150 | continue 151 | } 152 | } 153 | networkInfo := model.NetworkInfo{ 154 | Interface: netIO.Name, 155 | BytesSent: netIO.BytesSent, 156 | BytesRecv: netIO.BytesRecv, 157 | PacketsSent: netIO.PacketsSent, 158 | PacketsRecv: netIO.PacketsRecv, 159 | } 160 | info.NetworkInfo = append(info.NetworkInfo, networkInfo) 161 | } 162 | 163 | // 获取系统负载 164 | loadAvg, err := load.Avg() 165 | if err == nil { 166 | info.LoadInfo = model.LoadInfo{ 167 | Load1: loadAvg.Load1, 168 | Load5: loadAvg.Load5, 169 | Load15: loadAvg.Load15, 170 | } 171 | } 172 | 173 | return info, nil 174 | } 175 | 176 | // CollectBatch 在指定时间段内批量收集系统信息,现在只采集一条,以后再扩展 177 | func (c *DefaultCollector) CollectBatch(ctx context.Context) ([]*model.SystemInfo, error) { 178 | // 创建结果切片 179 | results := make([]*model.SystemInfo, 0) 180 | 181 | // 采集第一条数据 182 | info, err := c.Collect(ctx) 183 | if err != nil { 184 | return nil, err 185 | } 186 | results = append(results, info) 187 | 188 | return results, nil 189 | } 190 | -------------------------------------------------------------------------------- /agent/pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | ServerURL string = "" 5 | Token string = "" 6 | Interval int = 120 7 | ProxyURL string = "" 8 | ) 9 | -------------------------------------------------------------------------------- /agent/pkg/model/collector.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | // SystemInfo 包含系统的各种信息 6 | type SystemInfo struct { 7 | Token string `json:"token"` 8 | Timestamp time.Time `json:"timestamp"` 9 | Hostname string `json:"hostname"` 10 | Platform string `json:"platform"` 11 | OS string `json:"os"` 12 | Version string `json:"version"` // 操作系统版本 13 | IPAddresses []string `json:"ip_addresses"` // IP地址列表 14 | Keepalive int `json:"keepalive"` 15 | CPUInfo CPUInfo `json:"cpu"` 16 | MemoryInfo MemoryInfo `json:"memory"` 17 | DiskInfo []DiskInfo `json:"disks"` 18 | NetworkInfo []NetworkInfo `json:"network"` 19 | LoadInfo LoadInfo `json:"load"` 20 | } 21 | 22 | // CPUInfo 包含CPU相关信息 23 | type CPUInfo struct { 24 | Usage float64 `json:"usage"` 25 | Cores int `json:"cores"` 26 | ModelName string `json:"model_name"` 27 | } 28 | 29 | // MemoryInfo 包含内存相关信息 30 | type MemoryInfo struct { 31 | Total uint64 `json:"total"` 32 | Used uint64 `json:"used"` 33 | Free uint64 `json:"free"` 34 | UsageRate float64 `json:"usage_rate"` 35 | } 36 | 37 | // DiskInfo 包含磁盘相关信息 38 | type DiskInfo struct { 39 | Device string `json:"device"` 40 | MountPoint string `json:"mount_point"` 41 | Total uint64 `json:"total"` 42 | Used uint64 `json:"used"` 43 | Free uint64 `json:"free"` 44 | UsageRate float64 `json:"usage_rate"` 45 | FSType string `json:"fs_type"` 46 | } 47 | 48 | // NetworkInfo 包含网络相关信息 49 | type NetworkInfo struct { 50 | Interface string `json:"interface"` 51 | BytesSent uint64 `json:"bytes_sent"` 52 | BytesRecv uint64 `json:"bytes_recv"` 53 | PacketsSent uint64 `json:"packets_sent"` 54 | PacketsRecv uint64 `json:"packets_recv"` 55 | } 56 | 57 | // LoadInfo 包含系统负载信息 58 | type LoadInfo struct { 59 | Load1 float64 `json:"load1"` 60 | Load5 float64 `json:"load5"` 61 | Load15 float64 `json:"load15"` 62 | } 63 | -------------------------------------------------------------------------------- /agent/pkg/model/reporter.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // HTTPReporter 是基于HTTP的数据上报器实现 8 | type HTTPReporter struct { 9 | ServerURL string 10 | ApiToken string 11 | ProxyURL string 12 | Client *http.Client 13 | Registered bool 14 | } 15 | 16 | // RegisterPayload 定义注册到后端的数据结构 17 | type RegisterPayload struct { 18 | Token string `json:"token"` // API令牌 19 | Name string `json:"name"` // 客户端名称 20 | Hostname string `json:"hostname"` // 主机名 21 | IPAddresses []string `json:"ip_addresses"` // IP地址列表 22 | OS string `json:"os"` // 操作系统 23 | Version string `json:"version"` // 操作系统版本 24 | } 25 | 26 | // RegisterResponse 定义注册响应结构 27 | type RegisterResponse struct { 28 | Success bool `json:"success"` 29 | Message string `json:"message"` 30 | Agent struct { 31 | ID int `json:"id"` 32 | } `json:"agent"` 33 | } 34 | -------------------------------------------------------------------------------- /agent/pkg/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | package reporter 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "time" 14 | 15 | "github.com/xugou/agent/pkg/config" 16 | "github.com/xugou/agent/pkg/model" 17 | "github.com/xugou/agent/pkg/utils" 18 | ) 19 | 20 | // setDefaultHeaders 设置所有请求的通用头部 21 | func setDefaultHeaders(req *http.Request) { 22 | req.Header.Set("Content-Type", "application/json") 23 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") 24 | req.Header.Set("Referer", "https://www.google.com/") 25 | } 26 | 27 | // Reporter 定义数据上报器接口 28 | type Reporter interface { 29 | Report(ctx context.Context, info *model.SystemInfo) error // 上报单个采集的系统信息 30 | ReportBatch(ctx context.Context, infoList []*model.SystemInfo) error // 上报批量采集的系统信息 31 | } 32 | 33 | type DefaultReporter struct { 34 | reporter *model.HTTPReporter 35 | } 36 | 37 | func NewReporter() Reporter { 38 | return &DefaultReporter{ 39 | reporter: NewHTTPReporter(), 40 | } 41 | } 42 | 43 | // NewHTTPReporter 创建一个新的HTTP数据上报器 44 | func NewHTTPReporter() *model.HTTPReporter { 45 | // 创建HTTP客户端 46 | client := &http.Client{ 47 | Timeout: 10 * time.Second, 48 | } 49 | 50 | // 如果设置了代理,配置代理 51 | if config.ProxyURL != "" { 52 | proxy, err := url.Parse(config.ProxyURL) 53 | if err != nil { 54 | fmt.Printf("警告: 代理URL解析失败: %v,将不使用代理\n", err) 55 | } else { 56 | client.Transport = &http.Transport{ 57 | Proxy: http.ProxyURL(proxy), 58 | } 59 | } 60 | } 61 | 62 | reporter := &model.HTTPReporter{ 63 | ServerURL: utils.NormalizeURL(config.ServerURL), 64 | ApiToken: config.Token, 65 | ProxyURL: config.ProxyURL, 66 | Client: client, 67 | Registered: false, 68 | } 69 | 70 | return reporter 71 | } 72 | 73 | func (r *DefaultReporter) Report(ctx context.Context, info *model.SystemInfo) error { 74 | if !r.reporter.Registered { 75 | // 客户端未注册,先注册 76 | if err := r.register(ctx, info); err != nil { 77 | log.Printf("注册客户端失败: %v", err) 78 | return err 79 | } 80 | } 81 | reportURL := fmt.Sprintf("%s/api/agents/status", r.reporter.ServerURL) 82 | reportPaylod, err := json.Marshal(info) 83 | 84 | log.Println("即将上报数据: ", info) 85 | 86 | if err != nil { 87 | log.Println("注册客户端失败: ", err) 88 | return err 89 | } 90 | 91 | req, err := http.NewRequestWithContext(ctx, "POST", reportURL, bytes.NewBuffer(reportPaylod)) 92 | if err != nil { 93 | log.Println("创建请求失败:", err) 94 | return err 95 | } 96 | setDefaultHeaders(req) 97 | 98 | resp, err := r.reporter.Client.Do(req) 99 | if err != nil { 100 | log.Println("上报数据失败:", err) 101 | return err 102 | } 103 | defer resp.Body.Close() 104 | return nil 105 | } 106 | 107 | // ReportBatch 将多个系统信息批量上报到服务器 108 | func (r *DefaultReporter) ReportBatch(ctx context.Context, infoList []*model.SystemInfo) error { 109 | // 客户端未注册,先注册 110 | if err := r.register(ctx, infoList[0]); err != nil { 111 | log.Printf("注册客户端失败: %v", err) 112 | return err 113 | } 114 | 115 | reportURL := fmt.Sprintf("%s/api/agents/status", r.reporter.ServerURL) 116 | reportPaylod, err := json.Marshal(infoList) 117 | 118 | if err != nil { 119 | log.Println("注册客户端失败: ", err) 120 | return err 121 | } 122 | 123 | req, err := http.NewRequestWithContext(ctx, "POST", reportURL, bytes.NewBuffer(reportPaylod)) 124 | if err != nil { 125 | log.Println("创建请求失败:", err) 126 | return err 127 | } 128 | setDefaultHeaders(req) 129 | 130 | resp, err := r.reporter.Client.Do(req) 131 | if err != nil { 132 | log.Println("上报数据失败:", err) 133 | return err 134 | } 135 | defer resp.Body.Close() 136 | return nil 137 | } 138 | 139 | func (r *DefaultReporter) register(ctx context.Context, info *model.SystemInfo) error { 140 | 141 | log.Println("开始检查是否客户端已经注册,未注册将会自动注册") 142 | 143 | registerURL := fmt.Sprintf("%s/api/agents/register", r.reporter.ServerURL) 144 | registerPaylod := &model.RegisterPayload{ 145 | Token: config.Token, 146 | Name: info.Hostname, 147 | Hostname: info.Hostname, 148 | IPAddresses: utils.GetLocalIPs(), 149 | OS: info.OS, 150 | Version: info.Version, 151 | } 152 | 153 | data, err := json.Marshal(registerPaylod) 154 | 155 | if err != nil { 156 | log.Println("注册客户端失败: ", err) 157 | return err 158 | } 159 | 160 | req, err := http.NewRequestWithContext(ctx, "POST", registerURL, bytes.NewBuffer(data)) 161 | if err != nil { 162 | log.Println("创建请求失败: ", err) 163 | return err 164 | } 165 | setDefaultHeaders(req) 166 | 167 | resp, err := r.reporter.Client.Do(req) 168 | if err != nil { 169 | log.Println("注册客户端失败: ", err) 170 | return err 171 | } 172 | defer resp.Body.Close() 173 | 174 | body, err := io.ReadAll(resp.Body) 175 | 176 | if err != nil { 177 | log.Println("注册客户端失败: ", err) 178 | return err 179 | } 180 | 181 | var respData *model.RegisterResponse 182 | 183 | if err := json.Unmarshal(body, &respData); err != nil { 184 | log.Println("注册客户端失败: ", err) 185 | return err 186 | } 187 | 188 | if !respData.Success { 189 | log.Println("注册客户端失败: ", respData.Message) 190 | return errors.New(respData.Message) 191 | } 192 | 193 | log.Printf("客户端 ID: %d", respData.Agent.ID) 194 | 195 | r.reporter.Registered = true 196 | 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /agent/pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net" 4 | 5 | // NormalizeURL 处理URL格式,确保URL末尾没有斜杠 6 | func NormalizeURL(url string) string { 7 | // 移除URL末尾的斜杠 8 | if len(url) > 0 && url[len(url)-1] == '/' { 9 | return url[:len(url)-1] 10 | } 11 | return url 12 | } 13 | 14 | // GetLocalIPs 获取所有本地IPv4地址 15 | func GetLocalIPs() []string { 16 | ips := []string{} 17 | addrs, err := net.InterfaceAddrs() 18 | if err != nil { 19 | return append(ips, "unknown") 20 | } 21 | 22 | for _, addr := range addrs { 23 | // 检查IP地址类型 24 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 25 | if ipnet.IP.To4() != nil { 26 | ips = append(ips, ipnet.IP.String()) 27 | } 28 | } 29 | } 30 | 31 | if len(ips) == 0 { 32 | return append(ips, "unknown") 33 | } 34 | 35 | return ips 36 | } 37 | -------------------------------------------------------------------------------- /backend/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { defineConfig } from 'drizzle-kit'; 3 | 4 | export default defineConfig({ 5 | out: './drizzle', 6 | schema: './src/db/schema.ts', 7 | dialect: 'sqlite', 8 | driver: 'd1-http', 9 | strict: false, 10 | verbose: true, 11 | }); 12 | -------------------------------------------------------------------------------- /backend/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1748308256007, 9 | "tag": "0000_romantic_next_avengers", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.5", 4 | "main": "dist/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "wrangler dev --test-scheduled", 8 | "build": "tsc && npm run generate:migrations", 9 | "deploy": "wrangler deploy", 10 | "db:generate": "drizzle-kit generate", 11 | "generate:migrations": "tsx scripts/generate-migrations.ts" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "description": "", 17 | "dependencies": { 18 | "@hono/node-server": "^1.13.8", 19 | "@types/bcryptjs": "^2.4.6", 20 | "@types/jsonwebtoken": "^9.0.9", 21 | "@types/node": "^22.13.10", 22 | "bcryptjs": "^2.4.3", 23 | "dotenv": "^16.5.0", 24 | "drizzle-orm": "^0.44.1", 25 | "hono": "^4.7.4", 26 | "jsonwebtoken": "^9.0.2", 27 | "typescript": "^5.8.2" 28 | }, 29 | "devDependencies": { 30 | "@cloudflare/workers-types": "^4.20250317.0", 31 | "cross-env": "^7.0.3", 32 | "drizzle-kit": "^0.31.1", 33 | "nodemon": "^3.1.9", 34 | "ts-node": "^10.9.2", 35 | "tsx": "^4.19.4", 36 | "wrangler": "^4.13.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/scripts/generate-migrations.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | // ESM 模块中获取 __dirname 的替代方案 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = path.dirname(__filename); 8 | 9 | // 添加 IF NOT EXISTS 到各种 SQL 语句 10 | function addIfNotExists(sql: string): string { 11 | return sql 12 | // 处理 CREATE TABLE 语句 13 | .replace(/CREATE TABLE (`?\w+`?)/g, 'CREATE TABLE IF NOT EXISTS $1') 14 | // 处理 CREATE INDEX 语句 15 | .replace(/CREATE (UNIQUE )?INDEX (`?\w+`?)/g, 'CREATE $1INDEX IF NOT EXISTS $2') 16 | // 处理 ALTER TABLE ADD COLUMN 语句 17 | .replace( 18 | /ALTER TABLE (`?\w+`?) ADD (COLUMN )?(`?\w+`? \w+.*)/g, 19 | (match, table, _, column) => { 20 | // 提取列名(去掉类型和约束) 21 | const columnName = column.match(/^`?\w+`?/)[0]; 22 | return ` 23 | -- 检查列是否存在 24 | SELECT CASE 25 | WHEN EXISTS ( 26 | SELECT 1 FROM pragma_table_info(${table}) WHERE name = ${columnName} 27 | ) 28 | THEN 1 29 | ELSE ( 30 | ALTER TABLE ${table} ADD ${column} 31 | ) 32 | END;`; 33 | } 34 | ); 35 | } 36 | 37 | // 生成迁移文件 38 | async function generateMigrations() { 39 | const migrationsDir = path.join(__dirname, '..', 'drizzle'); 40 | const outputFile = path.join(__dirname, '..', 'src', 'db', 'generated-migrations.ts'); 41 | 42 | // 确保目录存在 43 | const outputDir = path.dirname(outputFile); 44 | if (!fs.existsSync(outputDir)) { 45 | fs.mkdirSync(outputDir, { recursive: true }); 46 | } 47 | 48 | // 读取所有 SQL 文件 49 | const files = fs.readdirSync(migrationsDir) 50 | .filter(file => file.endsWith('.sql')) 51 | .sort(); 52 | 53 | // 生成迁移数组 54 | const migrations = files.map(file => { 55 | const content = fs.readFileSync(path.join(migrationsDir, file), 'utf-8'); 56 | // 对每个 SQL 语句添加 IF NOT EXISTS 57 | const modifiedContent = content 58 | .split('--> statement-breakpoint') 59 | .map(stmt => addIfNotExists(stmt.trim())) 60 | .filter(stmt => stmt.length > 0) 61 | .join('\n--> statement-breakpoint\n'); 62 | 63 | return { 64 | name: file, 65 | content: modifiedContent.replace(/`/g, '\\`') // 转义反引号 66 | }; 67 | }); 68 | 69 | // 生成 TypeScript 文件内容 70 | const fileContent = `// 此文件由 generate-migrations.ts 自动生成 71 | // 请不要手动修改 72 | 73 | export interface Migration { 74 | name: string; 75 | sql: string; 76 | } 77 | 78 | export const MIGRATIONS: Migration[] = [ 79 | ${migrations.map(m => ` { 80 | name: "${m.name}", 81 | sql: \`${m.content}\` 82 | }`).join(',\n')} 83 | ]; 84 | `; 85 | 86 | // 写入文件 87 | fs.writeFileSync(outputFile, fileContent, 'utf-8'); 88 | console.log(`已生成迁移文件: ${outputFile}`); 89 | } 90 | 91 | generateMigrations().catch(console.error); -------------------------------------------------------------------------------- /backend/src/api/agents.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Bindings } from "../models/db"; 3 | import { Agent } from "../models/agent"; 4 | import { 5 | getAgents, 6 | getAgentDetail, 7 | updateAgentService, 8 | deleteAgentService, 9 | generateAgentToken, 10 | registerAgentService, 11 | updateAgentStatusService, 12 | getAgentMetrics, 13 | getLatestAgentMetrics, 14 | } from "../services/AgentService"; 15 | 16 | const agents = new Hono<{ 17 | Bindings: Bindings; 18 | Variables: { agent: Agent; jwtPayload: any }; 19 | }>(); 20 | 21 | // 获取所有客户端 22 | agents.get("/", async (c) => { 23 | const result = await getAgents(); 24 | 25 | return c.json( 26 | { 27 | success: result.success, 28 | agents: result.agents, 29 | message: result.message, 30 | }, 31 | result.status as any 32 | ); 33 | }); 34 | 35 | // 更新客户端信息 36 | agents.put("/:id", async (c) => { 37 | const agentId = Number(c.req.param("id")); 38 | const payload = c.get("jwtPayload"); 39 | const updateData = await c.req.json(); 40 | 41 | const result = await updateAgentService(c.env.DB, agentId, updateData); 42 | 43 | return c.json( 44 | { 45 | success: result.success, 46 | message: result.message, 47 | agent: result.agent, 48 | }, 49 | result.status as any 50 | ); 51 | }); 52 | 53 | // 删除客户端 54 | agents.delete("/:id", async (c) => { 55 | try { 56 | const agentId = Number(c.req.param("id")); 57 | 58 | await deleteAgentService(agentId); 59 | 60 | return c.json( 61 | { 62 | success: true, 63 | message: "客户端已删除", 64 | }, 65 | 200 66 | ); 67 | } catch (error) { 68 | return c.json( 69 | { 70 | success: false, 71 | message: error instanceof Error ? error.message : String(error), 72 | }, 73 | 500 74 | ); 75 | } 76 | }); 77 | 78 | // 生成客户端Token 79 | agents.post("/token/generate", async (c) => { 80 | // 生成新令牌 81 | const newToken = await generateAgentToken(c.env); 82 | 83 | // 可以选择将此token存储在临时表中,或者使用其他方式验证(例如,设置过期时间) 84 | // 这里为简化操作,只返回令牌 85 | 86 | return c.json({ 87 | success: true, 88 | message: "已生成客户端注册令牌", 89 | token: newToken, 90 | }); 91 | }); 92 | 93 | // 客户端自注册接口 94 | agents.post("/register", async (c) => { 95 | const { token, name, hostname, ip_addresses, os, version } = await c.req.json(); 96 | 97 | const result = await registerAgentService( 98 | c.env, 99 | token, 100 | name || "New Agent", 101 | hostname, 102 | ip_addresses, 103 | os, 104 | version 105 | ); 106 | 107 | return c.json( 108 | { 109 | success: result.success, 110 | message: result.message, 111 | agent: result.agent, 112 | }, 113 | result.status as any 114 | ); 115 | }); 116 | 117 | // 通过令牌更新客户端状态 118 | agents.post("/status", async (c) => { 119 | // 获取客户端发送的所有数据并打印日志 120 | const statusData = await c.req.json(); 121 | 122 | console.log("statusData: ", statusData); 123 | 124 | try { 125 | await updateAgentStatusService(statusData); 126 | return c.json( 127 | { 128 | success: true, 129 | message: "客户端状态已更新", 130 | }, 131 | 200 132 | ); 133 | } catch (error) { 134 | return c.json( 135 | { 136 | success: false, 137 | message: error instanceof Error ? error.message : String(error), 138 | }, 139 | 500 140 | ); 141 | } 142 | }); 143 | 144 | // 获取单个客户端的指标 145 | agents.get("/:id/metrics", async (c) => { 146 | const agentId = Number(c.req.param("id")); 147 | const result = await getAgentMetrics(agentId); 148 | return c.json( 149 | { 150 | success: true, 151 | agent: result, 152 | message: "获取客户端指标成功", 153 | }, 154 | 200 155 | ); 156 | }); 157 | 158 | // 获取单个客户端的最新指标 159 | agents.get("/:id/metrics/latest", async (c) => { 160 | const agentId = Number(c.req.param("id")); 161 | const result = await getLatestAgentMetrics(agentId); 162 | return c.json( 163 | { 164 | success: true, 165 | agent: result, 166 | message: "获取客户端最新指标成功", 167 | }, 168 | 200 169 | ); 170 | }); 171 | 172 | // 获取单个客户端 173 | agents.get("/:id", async (c) => { 174 | const agentId = Number(c.req.param("id")); 175 | 176 | const result = await getAgentDetail(agentId); 177 | 178 | console.log("result: ", result); 179 | 180 | return c.json( 181 | { 182 | success: true, 183 | agent: result, 184 | }, 185 | 200 186 | ); 187 | }); 188 | 189 | export { agents }; 190 | -------------------------------------------------------------------------------- /backend/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { jwtMiddleware } from "../middlewares"; 3 | import { 4 | loginUser, 5 | registerUser, 6 | getCurrentUser, 7 | } from "../services/AuthService"; 8 | import { Bindings } from "../models/db"; 9 | import { db } from "../config"; 10 | 11 | 12 | const auth = new Hono<{ Bindings: Bindings }>(); 13 | 14 | // 注册路由 15 | auth.post("/register", async (c) => { 16 | try { 17 | const { username, password, email } = await c.req.json(); 18 | 19 | // 调用 AuthService 的注册方法 20 | const result = await registerUser(c.env, username, password, email || null); 21 | 22 | return c.json( 23 | { 24 | success: result.success, 25 | message: result.message, 26 | user: result.user, 27 | }, 28 | result.success ? 201 : 400 29 | ); 30 | } catch (error) { 31 | console.error("注册错误:", error); 32 | return c.json({ success: false, message: "注册失败" }, 500); 33 | } 34 | }); 35 | 36 | // 登录路由 37 | auth.post("/login", async (c) => { 38 | try { 39 | console.log("=== 登录路由处理开始 ==="); 40 | console.log("c.env 类型:", typeof c.env); 41 | console.log("c.env 包含的键:", Object.keys(c.env)); 42 | console.log( 43 | "c.env.CF_VERSION_METADATA 是否存在:", 44 | !!c.env.CF_VERSION_METADATA 45 | ); 46 | 47 | if (c.env.CF_VERSION_METADATA) { 48 | console.log( 49 | "c.env.CF_VERSION_METADATA 内容:", 50 | JSON.stringify(c.env.CF_VERSION_METADATA, null, 2) 51 | ); 52 | } else { 53 | console.error("错误: c.env.CF_VERSION_METADATA 不存在于路由处理函数中"); 54 | } 55 | 56 | const { username, password } = await c.req.json(); 57 | console.log(`尝试用户登录: ${username}`); 58 | 59 | // 调用 AuthService 的登录方法 60 | const result = await loginUser(c.env, username, password); 61 | 62 | console.log("登录结果:", { 63 | success: result.success, 64 | message: result.message, 65 | hasToken: !!result.token, 66 | tokenLength: result.token ? result.token.length : 0, 67 | user: result.user, 68 | }); 69 | 70 | return c.json( 71 | { 72 | success: result.success, 73 | message: result.message, 74 | token: result.token, 75 | user: result.user, 76 | }, 77 | result.success ? 200 : 401 78 | ); 79 | } catch (error) { 80 | console.error("登录错误:", error); 81 | console.error( 82 | "错误堆栈:", 83 | error instanceof Error ? error.stack : "未知错误" 84 | ); 85 | return c.json({ success: false, message: "登录失败" }, 500); 86 | } 87 | }); 88 | 89 | // 获取当前用户信息 90 | auth.use("/me", jwtMiddleware); 91 | 92 | auth.get("/me", async (c) => { 93 | try { 94 | const payload = c.get("jwtPayload"); 95 | 96 | // 调用 AuthService 的获取当前用户方法 97 | const result = await getCurrentUser(c.env, payload.id); 98 | 99 | return c.json( 100 | { 101 | success: result.success, 102 | message: result.message, 103 | user: result.user, 104 | }, 105 | result.success ? 200 : 404 106 | ); 107 | } catch (error) { 108 | console.error("获取用户信息错误:", error); 109 | return c.json({ success: false, message: "获取用户信息失败" }, 500); 110 | } 111 | }); 112 | 113 | export { auth }; 114 | -------------------------------------------------------------------------------- /backend/src/api/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Bindings } from "../models/db"; 3 | import { getDashboardData } from "../services/DashboardService"; 4 | export const dashboard = new Hono<{ Bindings: Bindings }>(); 5 | 6 | // 获取仪表盘数据 7 | dashboard.get("/", async (c) => { 8 | const result = await getDashboardData(); 9 | return c.json( 10 | { 11 | monitors: result.monitors, 12 | agents: result.agents, 13 | }, 14 | 200 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /backend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./monitors"; 3 | export * from "./agents"; 4 | export * from "./users"; 5 | export * from "./status"; 6 | export * from "./notifications"; 7 | export * from "./dashboard"; 8 | -------------------------------------------------------------------------------- /backend/src/api/monitors.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Bindings } from "../models/db"; 3 | import * as MonitorService from "../services/MonitorService"; 4 | 5 | const monitors = new Hono<{ Bindings: Bindings }>(); 6 | 7 | // 获取所有监控 8 | monitors.get("/", async (c) => { 9 | const payload = c.get("jwtPayload"); 10 | 11 | // 调用服务层获取监控列表 12 | const result = await MonitorService.getAllMonitors(); 13 | 14 | return c.json( 15 | { 16 | success: result.success, 17 | monitors: result.monitors, 18 | }, 19 | result.status as any 20 | ); 21 | }); 22 | 23 | // 获取所有监控的每日统计数据 24 | monitors.get("/daily", async (c) => { 25 | // 调用服务层获取所有监控的每日统计数据 26 | const result = await MonitorService.getAllMonitorDailyStats(); 27 | 28 | return c.json({ 29 | success: result.success, 30 | dailyStats: result.dailyStats, 31 | message: result.message, 32 | }); 33 | }); 34 | 35 | // 创建监控 36 | monitors.post("/", async (c) => { 37 | const payload = c.get("jwtPayload"); 38 | const data = await c.req.json(); 39 | 40 | // 调用服务层创建监控 41 | const result = await MonitorService.createMonitor(data, payload.id); 42 | 43 | return c.json( 44 | { 45 | success: result.success, 46 | monitor: result.monitor, 47 | message: result.message, 48 | }, 49 | result.status as any 50 | ); 51 | }); 52 | 53 | // 获取所有监控状态历史 54 | monitors.get("/history", async (c) => { 55 | // 调用服务层获取监控历史 56 | const result = await MonitorService.getAllMonitorStatusHistory(); 57 | 58 | return c.json( 59 | { 60 | success: result.success, 61 | history: result.history, 62 | }, 63 | result.status as any 64 | ); 65 | }); 66 | 67 | // 获取单个监控 68 | monitors.get("/:id", async (c) => { 69 | const id = parseInt(c.req.param("id")); 70 | const payload = c.get("jwtPayload"); 71 | 72 | // 调用服务层获取监控详情 73 | const result = await MonitorService.getMonitorById(id); 74 | 75 | return c.json( 76 | { 77 | success: result.success, 78 | monitor: result.monitor, 79 | message: result.message, 80 | }, 81 | result.status as any 82 | ); 83 | }); 84 | 85 | // 更新监控 86 | monitors.put("/:id", async (c) => { 87 | const id = parseInt(c.req.param("id")); 88 | const data = await c.req.json(); 89 | 90 | // 调用服务层更新监控 91 | const result = await MonitorService.updateMonitor(id, data); 92 | 93 | return c.json( 94 | { 95 | success: result.success, 96 | monitor: result.monitor, 97 | message: result.message, 98 | }, 99 | result.status as any 100 | ); 101 | }); 102 | 103 | // 删除监控 104 | monitors.delete("/:id", async (c) => { 105 | const id = parseInt(c.req.param("id")); 106 | const payload = c.get("jwtPayload"); 107 | 108 | // 调用服务层删除监控 109 | const result = await MonitorService.deleteMonitor(id); 110 | 111 | return c.json( 112 | { 113 | success: result.success, 114 | message: result.message, 115 | }, 116 | result.status as any 117 | ); 118 | }); 119 | 120 | // 获取单个监控状态历史 121 | monitors.get("/:id/history", async (c) => { 122 | const id = parseInt(c.req.param("id")); 123 | const payload = c.get("jwtPayload"); 124 | 125 | // 调用服务层获取监控历史 126 | const result = await MonitorService.getMonitorStatusHistoryById( 127 | id, 128 | payload.id, 129 | payload.role 130 | ); 131 | 132 | return c.json( 133 | { 134 | success: result.success, 135 | history: result.history, 136 | message: result.message, 137 | }, 138 | result.status as any 139 | ); 140 | }); 141 | 142 | // 获取单个监控的每日统计数据 143 | monitors.get("/:id/daily", async (c) => { 144 | const id = parseInt(c.req.param("id")); 145 | 146 | // 调用服务层获取每日统计数据 147 | const result = await MonitorService.getMonitorDailyStats(id); 148 | 149 | return c.json({ 150 | success: result.success, 151 | dailyStats: result.dailyStats, 152 | message: result.message, 153 | }); 154 | }); 155 | 156 | // 手动检查单个监控 157 | monitors.post("/:id/check", async (c) => { 158 | const id = parseInt(c.req.param("id")); 159 | 160 | // 调用服务层手动检查监控 161 | const result = await MonitorService.manualCheckMonitor(id); 162 | 163 | return c.json( 164 | { 165 | success: result.success, 166 | message: result.message, 167 | result: result.result, 168 | }, 169 | result.status as any 170 | ); 171 | }); 172 | 173 | export { monitors }; 174 | -------------------------------------------------------------------------------- /backend/src/api/status.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Bindings } from "../models/db"; 3 | import { 4 | getStatusPageConfig, 5 | saveStatusPageConfig, 6 | getStatusPagePublicData, 7 | } from "../services/StatusService"; 8 | 9 | // 状态页配置接口 10 | interface StatusPageConfig { 11 | title: string; 12 | description: string; 13 | logoUrl: string; 14 | customCss: string; 15 | monitors: number[]; // 已选择的监控项ID 16 | agents: number[]; // 已选择的客户端ID 17 | } 18 | 19 | // 创建API路由 20 | const status = new Hono<{ Bindings: Bindings }>(); 21 | 22 | // 获取状态页配置(管理员) 23 | status.get("/config", async (c) => { 24 | const payload = c.get("jwtPayload"); 25 | const userId = payload.id; 26 | 27 | try { 28 | const config = await getStatusPageConfig(userId); 29 | return c.json(config); 30 | } catch (error) { 31 | console.error("获取状态页配置失败:", error); 32 | return c.json({ error: "获取状态页配置失败" }, 500); 33 | } 34 | }); 35 | 36 | // 保存状态页配置 37 | status.post("/config", async (c) => { 38 | const payload = c.get("jwtPayload"); 39 | const userId = payload.id; 40 | const data = (await c.req.json()) as StatusPageConfig; 41 | 42 | console.log("接收到的配置数据:", JSON.stringify(data)); 43 | 44 | if (!data) { 45 | console.log("无效的请求数据"); 46 | return c.json({ error: "无效的请求数据" }, 400); 47 | } 48 | 49 | try { 50 | const result = await saveStatusPageConfig(userId, data); 51 | return c.json(result); 52 | } catch (error) { 53 | console.error("保存状态页配置失败:", error); 54 | return c.json({ error: "保存状态页配置失败" }, 500); 55 | } 56 | }); 57 | 58 | status.get("/data", async (c) => { 59 | const result = await getStatusPagePublicData(); 60 | return c.json(result); 61 | }); 62 | 63 | export { status }; 64 | -------------------------------------------------------------------------------- /backend/src/api/users.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { Bindings } from "../models/db"; 3 | import { 4 | getAllUsersService, 5 | getUserByIdService, 6 | createUserService, 7 | updateUserService, 8 | deleteUserService, 9 | changePasswordService, 10 | } from "../services/UserService"; 11 | 12 | const users = new Hono<{ Bindings: Bindings }>(); 13 | 14 | // 获取所有用户(仅管理员) 15 | users.get("/", async (c) => { 16 | try { 17 | const payload = c.get("jwtPayload"); 18 | 19 | const result = await getAllUsersService(payload.role); 20 | 21 | return c.json( 22 | { success: result.success, users: result.users, message: result.message }, 23 | result.status as any 24 | ); 25 | } catch (error) { 26 | console.error("获取用户列表错误:", error); 27 | return c.json({ success: false, message: "获取用户列表失败" }, 500); 28 | } 29 | }); 30 | 31 | // 获取单个用户 32 | users.get("/:id", async (c) => { 33 | try { 34 | const id = parseInt(c.req.param("id")); 35 | const payload = c.get("jwtPayload"); 36 | 37 | const result = await getUserByIdService(id, payload.id, payload.role); 38 | 39 | return c.json( 40 | { success: result.success, user: result.user, message: result.message }, 41 | result.status as any 42 | ); 43 | } catch (error) { 44 | console.error("获取用户详情错误:", error); 45 | return c.json({ success: false, message: "获取用户详情失败" }, 500); 46 | } 47 | }); 48 | 49 | // 创建用户(仅管理员) 50 | users.post("/", async (c) => { 51 | try { 52 | const payload = c.get("jwtPayload"); 53 | const userData = await c.req.json(); 54 | 55 | const result = await createUserService(userData, payload.role); 56 | 57 | return c.json( 58 | { success: result.success, user: result.user, message: result.message }, 59 | result.status as any 60 | ); 61 | } catch (error) { 62 | console.error("创建用户错误:", error); 63 | return c.json({ success: false, message: "创建用户失败" }, 500); 64 | } 65 | }); 66 | 67 | // 更新用户 68 | users.put("/:id", async (c) => { 69 | try { 70 | const id = parseInt(c.req.param("id")); 71 | const payload = c.get("jwtPayload"); 72 | const updateData = await c.req.json(); 73 | 74 | const result = await updateUserService( 75 | id, 76 | updateData, 77 | payload.id, 78 | payload.role 79 | ); 80 | 81 | return c.json( 82 | { success: result.success, user: result.user, message: result.message }, 83 | result.status as any 84 | ); 85 | } catch (error) { 86 | console.error("更新用户错误:", error); 87 | return c.json({ success: false, message: "更新用户失败" }, 500); 88 | } 89 | }); 90 | 91 | // 删除用户(仅管理员) 92 | users.delete("/:id", async (c) => { 93 | try { 94 | const id = parseInt(c.req.param("id")); 95 | const payload = c.get("jwtPayload"); 96 | 97 | const result = await deleteUserService(id, payload.id, payload.role); 98 | 99 | return c.json( 100 | { success: result.success, message: result.message }, 101 | result.status as any 102 | ); 103 | } catch (error) { 104 | console.error("删除用户错误:", error); 105 | return c.json({ success: false, message: "删除用户失败" }, 500); 106 | } 107 | }); 108 | 109 | // 修改密码 110 | users.post("/:id/change-password", async (c) => { 111 | try { 112 | const id = parseInt(c.req.param("id")); 113 | const payload = c.get("jwtPayload"); 114 | const passwordData = await c.req.json(); 115 | 116 | const result = await changePasswordService( 117 | id, 118 | passwordData, 119 | payload.id, 120 | payload.role 121 | ); 122 | 123 | return c.json( 124 | { success: result.success, message: result.message }, 125 | result.status as any 126 | ); 127 | } catch (error) { 128 | console.error("修改密码错误:", error); 129 | return c.json({ success: false, message: "修改密码失败" }, 500); 130 | } 131 | }); 132 | 133 | export { users }; 134 | -------------------------------------------------------------------------------- /backend/src/config/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/d1"; 2 | import { Bindings } from "../models/db"; 3 | import * as schema from "../db/schema"; 4 | 5 | let db: any; 6 | 7 | export function initDb(env: Bindings) { 8 | db = drizzle( 9 | env.DB, 10 | { 11 | schema: { 12 | ...schema 13 | } 14 | } 15 | ); 16 | } 17 | 18 | export { db }; 19 | -------------------------------------------------------------------------------- /backend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./db"; -------------------------------------------------------------------------------- /backend/src/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./seed"; 2 | -------------------------------------------------------------------------------- /backend/src/db/migration.ts: -------------------------------------------------------------------------------- 1 | import { MIGRATIONS } from "./generated-migrations"; 2 | import { Bindings } from "../models"; 3 | 4 | 5 | const tableExists = async (d1: Bindings["DB"], tableName: string): Promise => { 6 | const result = await d1.prepare("SELECT * FROM sqlite_master WHERE type='table' AND name=?").bind(tableName).run(); 7 | return result.success && result.results && result.results.length > 0; 8 | } 9 | 10 | // 执行所有迁移脚本 11 | export async function runMigrations(d1: Bindings["DB"]): Promise { 12 | try { 13 | console.log("开始执行数据库迁移..."); 14 | // 检查迁移记录表是否存在 15 | const migrationsTableExists = await tableExists(d1, "migrations"); 16 | if (!migrationsTableExists) { 17 | // 创建迁移记录表 18 | await d1.prepare("CREATE TABLE migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, timestamp TEXT)").run(); 19 | console.log("迁移记录表创建成功"); 20 | } 21 | 22 | // 执行迁移 23 | for (const migration of MIGRATIONS) { 24 | // 检查是否已执行过该迁移 25 | const migrationResult = await d1.prepare("SELECT * FROM migrations WHERE name = ?").bind(migration.name).run(); 26 | if (migrationResult.results && migrationResult.results.length > 0) { 27 | console.log(`迁移 ${migration.name} 已执行过,跳过`); 28 | continue; 29 | } 30 | 31 | const result = await d1.prepare(migration.sql).run(); 32 | if (result.success) { 33 | console.log(`迁移 ${migration.name} 成功`); 34 | } else { 35 | console.error(`迁移 ${migration.name} 失败`); 36 | } 37 | // 写入迁移记录 38 | await d1.prepare("INSERT INTO migrations (name, timestamp) VALUES (?, ?)").bind(migration.name, new Date().toISOString()).run(); 39 | } 40 | 41 | console.log("所有迁移已完成"); 42 | } catch (error) { 43 | console.error("执行迁移脚本时出错:", error); 44 | throw error; 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono, ExecutionContext } from "hono"; 2 | import { logger } from "hono/logger"; 3 | import { prettyJSON } from "hono/pretty-json"; 4 | 5 | import * as seed from "./db"; 6 | import { Bindings } from "./models/db"; 7 | import * as middlewares from "./middlewares"; 8 | import * as jobs from "./jobs"; 9 | import * as api from "./api"; 10 | import * as config from "./config"; 11 | 12 | // 创建Hono应用 13 | const app = new Hono<{ Bindings: Bindings }>(); 14 | 15 | // 中间件,需要作为服务端接收所有来源客户端的请求 16 | app.use("*", logger()); 17 | app.use("*", middlewares.corsMiddleware); 18 | app.use("*", prettyJSON()); 19 | app.use("*", middlewares.jwtMiddleware); 20 | 21 | // 公共路由 22 | app.get("/", (c) => c.json({ message: "XUGOU API 服务正在运行" })); 23 | 24 | // 路由注册 25 | app.route("/api/auth", api.auth); 26 | app.route("/api/monitors", api.monitors); 27 | app.route("/api/agents", api.agents); 28 | app.route("/api/users", api.users); 29 | app.route("/api/status", api.status); 30 | app.route("/api/notifications", api.notifications); 31 | app.route("/api/dashboard", api.dashboard); 32 | // 数据库状态标志,用于记录数据库初始化状态 33 | let dbInitialized = false; 34 | 35 | // 导出 fetch 函数供 Cloudflare Workers 使用 36 | export default { 37 | // 处理 HTTP 请求 38 | async fetch(request: Request, env: Bindings, ctx: ExecutionContext) { 39 | 40 | // 如果是 OPTIONS 请求,直接处理 41 | if (request.method === "OPTIONS") { 42 | return new Response(null, { 43 | status: 204, 44 | headers: { 45 | "Access-Control-Allow-Origin": "*", 46 | "Access-Control-Allow-Methods": 47 | "GET, POST, PUT, DELETE, OPTIONS, PATCH", 48 | "Access-Control-Allow-Headers": 49 | "Content-Type, Authorization, X-Requested-With", 50 | "Access-Control-Max-Age": "86400", 51 | }, 52 | }); 53 | } 54 | 55 | 56 | // 初始化 drizzle 实例 57 | config.initDb(env); 58 | 59 | // 如果数据库尚未初始化,则进行初始化检查 60 | if (!dbInitialized) { 61 | console.log("首次请求,检查数据库状态..."); 62 | const initResult = await seed.checkAndInitializeDatabase(env.DB); 63 | dbInitialized = true; 64 | console.log("数据库检查结果:", initResult.message); 65 | } 66 | 67 | // 处理请求 68 | return app.fetch(request, env, ctx); 69 | }, 70 | 71 | // 添加定时任务,每分钟执行一次监控检查和客户端状态检查 72 | async scheduled(event: any, env: any, ctx: any) { 73 | try { 74 | await jobs.runScheduledTasks(event, env, ctx); 75 | } catch (error) { 76 | console.error("定时任务执行出错:", error); 77 | } 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /backend/src/jobs/index.ts: -------------------------------------------------------------------------------- 1 | // 导出所有定时任务 2 | import monitorTask from "./monitor-task"; 3 | import agentTask from "./agent-task"; 4 | import { Bindings } from "../models/db"; 5 | // 统一的定时任务处理函数 6 | export const runScheduledTasks = async (event: any, env: any, ctx: any) => { 7 | try { 8 | // 执行监控检查任务 9 | await monitorTask.scheduled(event, env, ctx); 10 | 11 | // 执行客户端状态检查任务 12 | await agentTask.scheduled(event, env, ctx); 13 | 14 | // 执行清理任务 - 每30天执行一次 15 | const now = new Date(); 16 | const dayOfMonth = now.getDate(); 17 | const hour = now.getHours(); 18 | const minute = now.getMinutes(); 19 | if (dayOfMonth === 1 && hour === 0 && minute === 30) { 20 | console.log("每月1号零点30分执行清理任务..."); 21 | await cleanupOldRecords(env.DB); 22 | } 23 | } catch (error) { 24 | console.error("定时任务执行出错:", error); 25 | } 26 | }; 27 | 28 | // 清理30天以前的历史记录 29 | export async function cleanupOldRecords(db: Bindings["DB"]) { 30 | console.log("开始清理30天以前的历史记录..."); 31 | 32 | // 清理30天以前的 monitor_daily_stats 33 | await db.prepare( 34 | ` 35 | DELETE FROM monitor_daily_stats 36 | WHERE date < datetime('now', '-30 days') 37 | ` 38 | ) 39 | .run(); 40 | 41 | // 清理通知历史记录 42 | await db 43 | .prepare( 44 | ` 45 | DELETE FROM notification_history 46 | WHERE sent_at < datetime('now', '-30 days') 47 | ` 48 | ) 49 | .run(); 50 | 51 | return { 52 | success: true, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from "hono"; 2 | import { jwt } from "hono/jwt"; 3 | import { getJwtSecret } from "../utils/jwt"; 4 | 5 | /** 6 | * JWT认证中间件 7 | * 验证请求中的JWT令牌并将解码的payload存入上下文 8 | */ 9 | export const jwtMiddleware = async (c: Context, next: Next) => { 10 | if ( 11 | (c.req.path.endsWith("/status") || 12 | c.req.path.endsWith("/register") || 13 | c.req.path.endsWith("/login")) && 14 | c.req.method === "POST" 15 | ) { 16 | return next(); 17 | } 18 | 19 | if (c.req.path.endsWith("/data") && c.req.method === "GET") { 20 | return next(); 21 | } 22 | // 获取 metrics 时暂时先不验证。 23 | if ( 24 | (c.req.path.endsWith("/metrics") || 25 | c.req.path.endsWith("/metrics/latest")) && 26 | c.req.method === "GET" 27 | ) { 28 | return next(); 29 | } 30 | const middleware = jwt({ 31 | secret: getJwtSecret(c), 32 | }); 33 | return middleware(c, next); 34 | }; 35 | -------------------------------------------------------------------------------- /backend/src/middlewares/cors.ts: -------------------------------------------------------------------------------- 1 | import { Context, Next } from "hono"; 2 | import { cors as honoCors } from "hono/cors"; 3 | 4 | /** 5 | * CORS中间件 6 | * 处理跨域资源共享并设置必要的响应头 7 | */ 8 | export const corsMiddleware = async (c: Context, next: Next) => { 9 | // 如果是 OPTIONS 请求,直接返回成功响应 10 | if (c.req.method === "OPTIONS") { 11 | c.header("Access-Control-Allow-Origin", "*"); 12 | c.header( 13 | "Access-Control-Allow-Methods", 14 | "GET, POST, PUT, DELETE, OPTIONS, PATCH" 15 | ); 16 | c.header( 17 | "Access-Control-Allow-Headers", 18 | "Content-Type, Authorization, X-Requested-With" 19 | ); 20 | c.header("Access-Control-Max-Age", "86400"); 21 | return new Response(null, { status: 204 }); 22 | } 23 | 24 | // 使用CORS中间件 25 | const corsHandler = honoCors({ 26 | origin: "*", // 允许所有来源 27 | allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], 28 | allowHeaders: ["Content-Type", "Authorization", "X-Requested-With"], 29 | exposeHeaders: ["Content-Length", "Content-Type"], 30 | maxAge: 86400, 31 | }); 32 | 33 | // 先执行CORS中间件 34 | await corsHandler(c, next); 35 | 36 | // 确保响应头设置正确 37 | c.header("Access-Control-Allow-Origin", "*"); 38 | }; 39 | -------------------------------------------------------------------------------- /backend/src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | // 导出所有中间件 2 | export * from "./auth"; 3 | export * from "./cors"; 4 | -------------------------------------------------------------------------------- /backend/src/models/agent.ts: -------------------------------------------------------------------------------- 1 | // 客户端类型定义 2 | export interface Agent { 3 | id: number; 4 | name: string; 5 | token: string; 6 | created_by: number; 7 | status: string | null; 8 | created_at: string; 9 | updated_at: string; 10 | hostname: string | null; 11 | keepalive: string | null; 12 | ip_addresses: string | null; // 存储多个IP地址的JSON字符串 13 | os: string | null; 14 | version: string | null; 15 | } 16 | 17 | // 客户端类型定义 18 | export interface AgentWithMetrics { 19 | id: number; 20 | name: string; 21 | token: string; 22 | created_by: number; 23 | status: string; 24 | created_at: string; 25 | updated_at: string; 26 | hostname: string | null; 27 | keepalive: string | null; 28 | ip_addresses: string | null; // 存储多个IP地址的JSON字符串 29 | os: string | null; 30 | version: string | null; 31 | metrics: Metrics[] | null; 32 | } 33 | 34 | export interface Metrics { 35 | agent_id: number; 36 | timestamp: string; 37 | cpu_usage: number; 38 | cpu_cores: number; 39 | cpu_model: string; 40 | memory_total: number; 41 | memory_used: number; 42 | memory_free: number; 43 | memory_usage_rate: number; 44 | load_1: number; 45 | load_5: number; 46 | load_15: number; 47 | disk_metrics: string; 48 | network_metrics: string; 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/models/db.ts: -------------------------------------------------------------------------------- 1 | // 通用数据库类型定义 2 | export interface D1Database { 3 | prepare(query: string): D1PreparedStatement; 4 | dump(): Promise; 5 | batch(statements: D1PreparedStatement[]): Promise[]>; 6 | exec(query: string): Promise>; 7 | } 8 | 9 | export interface D1PreparedStatement { 10 | bind(...values: any[]): D1PreparedStatement; 11 | first(colName?: string): Promise; 12 | run(): Promise>; 13 | all(): Promise>; 14 | raw(): Promise; 15 | } 16 | 17 | // 扩展D1Result meta属性的类型 18 | export interface D1Meta { 19 | last_row_id?: number; 20 | changes?: number; 21 | } 22 | 23 | export interface D1Result { 24 | results?: T[]; 25 | success: boolean; 26 | error?: string; 27 | meta?: object; 28 | } 29 | 30 | // 版本元数据类型定义 31 | export interface VersionMetadata { 32 | id?: string; 33 | tag?: string; 34 | timestamp?: string; 35 | [key: string]: any; 36 | } 37 | 38 | // 通用绑定类型 39 | export type Bindings = { 40 | DB: D1Database; 41 | CF_VERSION_METADATA?: VersionMetadata; 42 | }; 43 | -------------------------------------------------------------------------------- /backend/src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent'; 2 | export * from './db'; 3 | export * from './monitor'; 4 | export * from './notification'; 5 | export * from './user'; 6 | export * from './status'; -------------------------------------------------------------------------------- /backend/src/models/monitor.ts: -------------------------------------------------------------------------------- 1 | // 监控类型定义 2 | export interface Monitor { 3 | id: number; 4 | name: string; 5 | url: string; 6 | method: string; 7 | interval: number; 8 | timeout: number; 9 | expected_status: number; 10 | headers: Record; 11 | body: string; 12 | created_by: number; 13 | active: boolean; 14 | status: string; 15 | response_time: number; 16 | last_checked: string; 17 | created_at: string; 18 | updated_at: string; 19 | } 20 | 21 | // 监控历史记录类型 22 | export interface MonitorStatusHistory { 23 | id: number; 24 | monitor_id: number; 25 | status: string; 26 | timestamp: string; 27 | response_time: number; 28 | status_code: number; 29 | error: string | null; 30 | } 31 | 32 | // 监控每日统计类型 33 | export interface MonitorDailyStats { 34 | date: string; 35 | total_checks: number; 36 | up_checks: number; 37 | down_checks: number; 38 | avg_response_time: number; 39 | min_response_time: number; 40 | max_response_time: number; 41 | availability: number; 42 | monitor_id: number; 43 | created_at: string; 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/models/notification.ts: -------------------------------------------------------------------------------- 1 | // 通知渠道类型定义 2 | export interface NotificationChannel { 3 | id: number; 4 | name: string; 5 | type: string; // email, telegram 6 | config: string; // JSON字符串 7 | enabled: boolean; 8 | created_by: number; 9 | created_at: string; 10 | updated_at: string; 11 | } 12 | 13 | // 通知模板类型定义 14 | export interface NotificationTemplate { 15 | id: number; 16 | name: string; 17 | type: string; // default, custom 18 | subject: string; 19 | content: string; 20 | is_default: boolean; 21 | created_by: number; 22 | created_at: string; 23 | updated_at: string; 24 | } 25 | 26 | // 统一通知设置类型定义 27 | export interface NotificationSettings { 28 | id: number; 29 | user_id: number; 30 | target_type: string; // global-monitor, global-agent, monitor, agent 31 | target_id: number | 0; // 当target_type为monitor或agent时有效,存储monitor_id或agent_id 32 | 33 | enabled: boolean; 34 | on_down: boolean; // 适用于monitor类型 35 | on_recovery: boolean; // 适用于monitor和agent类型 36 | 37 | on_offline: boolean; // 适用于agent类型 38 | on_cpu_threshold: boolean; // 适用于agent类型 39 | cpu_threshold: number; // 适用于agent类型 40 | on_memory_threshold: boolean; // 适用于agent类型 41 | memory_threshold: number; // 适用于agent类型 42 | on_disk_threshold: boolean; // 适用于agent类型 43 | disk_threshold: number; // 适用于agent类型 44 | 45 | channels: string; // JSON字符串数组,存储channel IDs 46 | 47 | created_at: string; 48 | updated_at: string; 49 | } 50 | 51 | // 通知历史记录类型定义 52 | export interface NotificationHistory { 53 | id: number; 54 | type: string; // monitor, agent, system 55 | target_id: number | null; // monitor_id或agent_id,系统通知为null 56 | channel_id: number; 57 | template_id: number; 58 | status: string; // success, failed, pending 59 | content: string; 60 | error: string | null; 61 | sent_at: string; 62 | } 63 | 64 | // 前端接口所需的通知配置类型 65 | export interface NotificationConfig { 66 | channels: NotificationChannel[]; 67 | templates: NotificationTemplate[]; 68 | settings: { 69 | monitors: { 70 | enabled: boolean; 71 | onDown: boolean; 72 | onRecovery: boolean; 73 | channels: string[]; 74 | }; 75 | agents: { 76 | enabled: boolean; 77 | onOffline: boolean; 78 | onRecovery: boolean; 79 | onCpuThreshold: boolean; 80 | cpuThreshold: number; 81 | onMemoryThreshold: boolean; 82 | memoryThreshold: number; 83 | onDiskThreshold: boolean; 84 | diskThreshold: number; 85 | channels: string[]; 86 | }; 87 | specificMonitors: { 88 | [monitorId: string]: { 89 | enabled: boolean; 90 | onDown: boolean; 91 | onRecovery: boolean; 92 | channels: string[]; 93 | }; 94 | }; 95 | specificAgents: { 96 | [agentId: string]: { 97 | enabled: boolean; 98 | onOffline: boolean; 99 | onRecovery: boolean; 100 | onCpuThreshold: boolean; 101 | cpuThreshold: number; 102 | onMemoryThreshold: boolean; 103 | memoryThreshold: number; 104 | onDiskThreshold: boolean; 105 | diskThreshold: number; 106 | channels: string[]; 107 | }; 108 | }; 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /backend/src/models/status.ts: -------------------------------------------------------------------------------- 1 | // 状态页配置接口定义 2 | export interface StatusPageConfig { 3 | id?: number; 4 | user_id: number; 5 | title: string; 6 | description: string; 7 | logo_url: string; 8 | custom_css: string; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/models/user.ts: -------------------------------------------------------------------------------- 1 | // 用户类型定义 2 | export interface User { 3 | id: number; 4 | username: string; 5 | password: string; 6 | email: string | null; 7 | role: string; 8 | created_at: string; 9 | updated_at: string; 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/repositories/agent.ts: -------------------------------------------------------------------------------- 1 | import { Bindings } from "../models/db"; 2 | import { Agent, Metrics } from "../models/agent"; 3 | import { agents, agentMetrics24h } from "../db/schema"; 4 | import { db } from "../config"; 5 | import { desc, eq, inArray } from "drizzle-orm"; 6 | /** 7 | * 客户端相关的数据库操作 8 | */ 9 | 10 | // 获取所有客户端 11 | export async function getAllAgents() { 12 | return await db.select().from(agents).orderBy(desc(agents.created_at)); 13 | } 14 | 15 | // 批量获取客户端详情 16 | export async function getAgentsByIds(agentIds: number[]) { 17 | if (agentIds.length === 0) { 18 | return { results: [] }; 19 | } 20 | return await db.select().from(agents).where(inArray(agents.id, agentIds)); 21 | } 22 | 23 | // 批量获取客户端指标 24 | export async function getAgentMetricsByIds(agentIds: number[]) { 25 | if (agentIds.length === 0) { 26 | return { results: [] }; 27 | } 28 | return await db 29 | .select() 30 | .from(agentMetrics24h) 31 | .where(inArray(agentMetrics24h.agent_id, agentIds)); 32 | } 33 | 34 | // 获取单个客户端详情 35 | export async function getAgentById(id: number) { 36 | const agent = await db.select().from(agents).where(eq(agents.id, id)).limit(1); 37 | return agent[0]; 38 | } 39 | 40 | // 创建新客户端 41 | export async function createAgent( 42 | name: string, 43 | token: string, 44 | createdBy: number, 45 | status: string = "inactive", 46 | hostname: string | null = null, 47 | os: string | null = null, 48 | version: string | null = null, 49 | ipAddresses: string[] | null = null, 50 | keepalive: string | null = null 51 | ) { 52 | const now = new Date().toISOString(); 53 | 54 | // 将 ipAddresses 数组转换为 JSON 字符串 55 | const ipAddressesJson = ipAddresses ? JSON.stringify(ipAddresses) : null; 56 | 57 | const result = await db.insert(agents).values({ 58 | name, 59 | token, 60 | created_by: createdBy, 61 | status, 62 | created_at: now, 63 | updated_at: now, 64 | hostname, 65 | ip_addresses: ipAddressesJson, 66 | os, 67 | version, 68 | keepalive, 69 | }).returning(); 70 | 71 | if (!result) { 72 | throw new Error("创建客户端失败"); 73 | } 74 | return result[0]; 75 | } 76 | 77 | // 更新客户端信息 78 | export async function updateAgent( 79 | agent: Agent 80 | ) { 81 | agent.updated_at = new Date().toISOString(); 82 | 83 | try { 84 | const updatedAgent = await db.update(agents).set(agent).where(eq(agents.id, agent.id)).returning(); 85 | return updatedAgent[0]; 86 | } catch (error) { 87 | console.error("更新客户端失败:", error); 88 | throw new Error("更新客户端失败"); 89 | } 90 | } 91 | 92 | // 删除客户端 93 | export async function deleteAgent(id: number) { 94 | // 先删除关联的指标数据 95 | await db.delete(agentMetrics24h).where(eq(agentMetrics24h.agent_id, id)); 96 | 97 | const result = await db.delete(agents).where(eq(agents.id, id)); 98 | 99 | if (!result.success) { 100 | throw new Error("删除客户端失败"); 101 | } 102 | 103 | return { success: true, message: "客户端已删除" }; 104 | } 105 | 106 | // 通过令牌获取客户端 107 | export async function getAgentByToken(token: string) { 108 | const agent = await db.select().from(agents).where(eq(agents.token, token)); 109 | return agent[0]; 110 | } 111 | 112 | // 获取活跃状态的客户端 113 | export async function getActiveAgents() { 114 | return await db 115 | .select("id", "name", "updated_at", "keepalive") 116 | .from(agents) 117 | .where(eq(agents.status, "active")); 118 | } 119 | 120 | // 设置客户端为离线状态 121 | export async function setAgentInactive(id: number) { 122 | const now = new Date().toISOString(); 123 | 124 | return await db 125 | .update(agents) 126 | .set({ status: "inactive", updated_at: now }) 127 | .where(eq(agents.id, id)); 128 | } 129 | 130 | // 插入客户端资源指标 131 | export async function insertAgentMetrics(metrics: Metrics[]) { 132 | return await db.batch(metrics.map(metric => db.insert(agentMetrics24h).values(metric))); 133 | } 134 | 135 | // 获取指定客户端资源指标 136 | export async function getAgentMetrics(agentId: number) { 137 | return await db 138 | .select() 139 | .from(agentMetrics24h) 140 | .where(eq(agentMetrics24h.agent_id, agentId)); 141 | } 142 | 143 | // 获取指定客户端的最新指标 144 | export async function getLatestAgentMetrics(agentId: number) { 145 | const metrics = await db 146 | .select() 147 | .from(agentMetrics24h) 148 | .where(eq(agentMetrics24h.agent_id, agentId)) 149 | .orderBy(desc(agentMetrics24h.timestamp)) 150 | .limit(1); 151 | return metrics[0]; 152 | } 153 | -------------------------------------------------------------------------------- /backend/src/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent'; 2 | export * from './users'; 3 | export * from './status'; 4 | export * from './notification'; 5 | export * from './monitor'; 6 | -------------------------------------------------------------------------------- /backend/src/repositories/status.ts: -------------------------------------------------------------------------------- 1 | import { StatusPageConfig, Agent, Bindings } from "../models"; 2 | import { db } from "../config"; 3 | import { 4 | statusPageConfig, 5 | statusPageMonitors, 6 | statusPageAgents, 7 | } from "../db/schema"; 8 | import { eq, desc, asc, and, count, sql } from "drizzle-orm"; 9 | 10 | /** 11 | * 状态页相关的数据库操作 12 | */ 13 | 14 | // 获取所有状态页配置 15 | export async function getAllStatusPageConfigs() { 16 | return await db.select().from(statusPageConfig); 17 | } 18 | 19 | // 获取配置的监控项 20 | export async function getConfigMonitors( 21 | configId: number, 22 | ) { 23 | 24 | return await db 25 | .select() 26 | .from(statusPageMonitors) 27 | .where(eq(statusPageMonitors.config_id, configId)); 28 | } 29 | 30 | // 获取配置的客户端 31 | export async function getConfigAgents( 32 | 33 | configId: number, 34 | ) { 35 | return await db.select().from(statusPageAgents).where(eq(statusPageAgents.config_id, configId)); 36 | } 37 | 38 | // 获取状态页配置 39 | export async function getStatusPageConfigById(id: number) { 40 | const config = await db 41 | .select() 42 | .from(statusPageConfig) 43 | .where(eq(statusPageConfig.id, id)); 44 | return config[0]; 45 | } 46 | 47 | // 更新状态页配置 48 | export async function updateStatusPageConfig( 49 | id: number, 50 | title: string, 51 | description: string, 52 | logoUrl: string, 53 | customCss: string 54 | ) { 55 | return await db 56 | .update(statusPageConfig) 57 | .set({ 58 | title: title, 59 | description: description, 60 | logo_url: logoUrl, 61 | custom_css: customCss, 62 | }) 63 | .where(eq(statusPageConfig.id, id)); 64 | } 65 | 66 | // 创建状态页配置 67 | export async function createStatusPageConfig( 68 | userId: number, 69 | title: string, 70 | description: string, 71 | logoUrl: string, 72 | customCss: string 73 | ) { 74 | const result = await db.insert(statusPageConfig).values({ 75 | user_id: userId, 76 | title: title, 77 | description: description, 78 | logo_url: logoUrl, 79 | custom_css: customCss, 80 | }); 81 | 82 | if (!result.success) { 83 | throw new Error("创建状态页配置失败"); 84 | } 85 | 86 | // 获取新插入的ID 87 | return result.meta.last_row_id; 88 | } 89 | 90 | // 清除配置的监控项关联 91 | export async function clearConfigMonitorLinks(configId: number) { 92 | return await db 93 | .delete(statusPageMonitors) 94 | .where(eq(statusPageMonitors.config_id, configId)); 95 | } 96 | 97 | // 清除配置的客户端关联 98 | export async function clearConfigAgentLinks(configId: number) { 99 | return await db 100 | .delete(statusPageAgents) 101 | .where(eq(statusPageAgents.config_id, configId)); 102 | } 103 | 104 | // 添加监控项到配置 105 | export async function addMonitorToConfig(configId: number, monitorId: number) { 106 | return await db.insert(statusPageMonitors).values({ 107 | config_id: configId, 108 | monitor_id: monitorId, 109 | }); 110 | } 111 | 112 | // 添加客户端到配置 113 | export async function addAgentToConfig(configId: number, agentId: number) { 114 | return await db.insert(statusPageAgents).values({ 115 | config_id: configId, 116 | agent_id: agentId, 117 | }); 118 | } 119 | 120 | // 获取选中的监控项IDs 121 | export async function getSelectedMonitors(configId: number) { 122 | return await db 123 | .select({ monitor_id: statusPageMonitors.monitor_id }) 124 | .from(statusPageMonitors) 125 | .where(eq(statusPageMonitors.config_id, configId)); 126 | } 127 | 128 | // 获取选中的客户端IDs 129 | export async function getSelectedAgents(configId: number) { 130 | return await db 131 | .select({ agent_id: statusPageAgents.agent_id }) 132 | .from(statusPageAgents) 133 | .where(eq(statusPageAgents.config_id, configId)); 134 | } 135 | -------------------------------------------------------------------------------- /backend/src/repositories/users.ts: -------------------------------------------------------------------------------- 1 | import { eq, asc, and } from "drizzle-orm"; 2 | 3 | import { Bindings } from "../models/db"; 4 | import { User } from "../models"; 5 | import { db } from "../config"; 6 | import { users } from "../db/schema"; 7 | 8 | // 不含密码的用户信息 9 | type UserWithoutPassword = Omit; 10 | 11 | /** 12 | * 用户管理相关的数据库操作 13 | */ 14 | 15 | // 获取所有用户(不包括密码) 16 | export async function getAllUsers() { 17 | return await db 18 | .select({ 19 | id: users.id, 20 | username: users.username, 21 | email: users.email, 22 | role: users.role, 23 | created_at: users.created_at, 24 | updated_at: users.updated_at, 25 | }) 26 | .from(users) 27 | .orderBy(asc(users.id)) 28 | .then((result: User[]) => result); 29 | } 30 | 31 | // 根据ID获取用户(不包括密码) 32 | export async function getUserById(id: number) { 33 | return await db 34 | .select({ 35 | id: users.id, 36 | username: users.username, 37 | email: users.email, 38 | role: users.role, 39 | created_at: users.created_at, 40 | updated_at: users.updated_at, 41 | }) 42 | .from(users) 43 | .where(eq(users.id, id)) 44 | .limit(1) 45 | .then((result: User[]) => result[0] || null); 46 | } 47 | 48 | // 根据ID获取完整用户信息(包括密码) 49 | export async function getFullUserById(id: number) { 50 | return await db 51 | .select() 52 | .from(users) 53 | .where(eq(users.id, id)) 54 | .limit(1) 55 | .then((result: User[]) => result[0] || null); 56 | } 57 | 58 | // 根据用户名检查用户是否存在 59 | export async function checkUserExists(username: string) { 60 | return await db 61 | .select({ id: users.id }) 62 | .from(users) 63 | .where(eq(users.username, username)) 64 | .limit(1) 65 | .then((result: User[]) => result[0] || null); 66 | } 67 | 68 | // 创建新用户 69 | export async function createUser( 70 | username: string, 71 | hashedPassword: string, 72 | email: string | null, 73 | role: string 74 | ) { 75 | const now = new Date().toISOString(); 76 | 77 | const result = await db 78 | .insert(users) 79 | .values({ 80 | username: username, 81 | password: hashedPassword, 82 | email: email, 83 | role: role, 84 | created_at: now, 85 | updated_at: now, 86 | }) 87 | .returning(); 88 | 89 | if (!result.success) { 90 | throw new Error("创建用户失败"); 91 | } 92 | 93 | return result[0]; 94 | } 95 | 96 | // 更新用户信息 97 | export async function updateUser( 98 | id: number, 99 | updates: { 100 | username?: string; 101 | email?: string | null; 102 | role?: string; 103 | password?: string; 104 | } 105 | ) { 106 | const now = new Date().toISOString(); 107 | 108 | // 准备更新数据 109 | const fieldsToUpdate = []; 110 | const values = []; 111 | 112 | if (updates.username !== undefined) { 113 | fieldsToUpdate.push("username = ?"); 114 | values.push(updates.username); 115 | } 116 | 117 | if (updates.email !== undefined) { 118 | fieldsToUpdate.push("email = ?"); 119 | values.push(updates.email); 120 | } 121 | 122 | if (updates.role !== undefined) { 123 | fieldsToUpdate.push("role = ?"); 124 | values.push(updates.role); 125 | } 126 | 127 | if (updates.password !== undefined) { 128 | fieldsToUpdate.push("password = ?"); 129 | values.push(updates.password); 130 | } 131 | 132 | fieldsToUpdate.push("updated_at = ?"); 133 | values.push(now); 134 | 135 | // 添加ID作为条件 136 | values.push(id); 137 | 138 | // 如果没有要更新的字段,返回错误 139 | if (fieldsToUpdate.length <= 1) { 140 | throw new Error("没有提供要更新的字段"); 141 | } 142 | 143 | // 执行更新 144 | const result = await db 145 | .update(users) 146 | .set({ 147 | username: updates.username, 148 | email: updates.email, 149 | role: updates.role, 150 | password: updates.password, 151 | updated_at: now, 152 | }) 153 | .where(eq(users.id, id)) 154 | .returning(); 155 | 156 | if (!result.success) { 157 | throw new Error("更新用户失败"); 158 | } 159 | 160 | return result[0]; 161 | } 162 | 163 | // 更新用户密码 164 | export async function updateUserPassword(id: number, hashedPassword: string) { 165 | const now = new Date().toISOString(); 166 | 167 | const result = await db 168 | .update(users) 169 | .set({ 170 | password: hashedPassword, 171 | updated_at: now, 172 | }) 173 | .where(eq(users.id, id)) 174 | .returning(); 175 | 176 | if (!result.success) { 177 | throw new Error("更新密码失败"); 178 | } 179 | 180 | return { success: true, message: "密码已更新" }; 181 | } 182 | 183 | // 删除用户 184 | export async function deleteUser(id: number) { 185 | const result = await db.delete(users).where(eq(users.id, id)).returning(); 186 | 187 | if (!result.success) { 188 | throw new Error("删除用户失败"); 189 | } 190 | 191 | return { success: true, message: "用户已删除" }; 192 | } 193 | 194 | // 根据用户名获取用户 195 | export async function getUserByUsername( 196 | username: string 197 | ): Promise { 198 | return await db 199 | .select() 200 | .from(users) 201 | .where(eq(users.username, username)) 202 | .limit(1) 203 | .then((result: User[]) => result[0] || null); 204 | } 205 | 206 | // 获取管理员用户ID 207 | export async function getAdminUserId() { 208 | const adminId = await db 209 | .select({ id: users.id }) 210 | .from(users) 211 | .where(eq(users.username, "admin")); 212 | 213 | if (!adminId) { 214 | throw new Error("无法找到管理员用户"); 215 | } 216 | 217 | return adminId[0].id; 218 | } 219 | -------------------------------------------------------------------------------- /backend/src/services/AuthService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthService.ts 3 | * 认证服务,处理用户认证、注册和令牌管理相关的业务逻辑 4 | */ 5 | 6 | import * as bcrypt from "bcryptjs"; 7 | import * as jsonwebtoken from "jsonwebtoken"; 8 | import * as repositories from "../repositories"; 9 | import { getJwtSecret } from "../utils/jwt"; 10 | import { Bindings } from "../models/db"; 11 | 12 | /** 13 | * 用户登录 14 | * @param env 环境变量,包含数据库连接 15 | * @param username 用户名 16 | * @param password 密码(明文) 17 | * @returns 登录结果,包含令牌和用户信息 18 | */ 19 | export async function loginUser( 20 | env: { DB: Bindings["DB"] } & any, 21 | username: string, 22 | password: string 23 | ): Promise<{ success: boolean; message: string; token?: string; user?: any }> { 24 | try { 25 | console.log("=== loginUser 函数被调用 ==="); 26 | console.log("传入的 env 参数类型:", typeof env); 27 | console.log( 28 | "传入的 env 参数结构:", 29 | JSON.stringify( 30 | env, 31 | (key, value) => { 32 | // 排除可能的循环引用对象 33 | if (key === "DB" && typeof value === "object") { 34 | return "[DB Object]"; 35 | } 36 | return value; 37 | }, 38 | 2 39 | ) 40 | ); 41 | 42 | // 查找用户 43 | console.log("开始查找用户:", username); 44 | const user = await repositories.getUserByUsername(username); 45 | 46 | if (!user) { 47 | console.log("用户不存在:", username); 48 | return { success: false, message: "用户名或密码错误" }; 49 | } 50 | 51 | console.log("找到用户, ID:", user.id); 52 | 53 | // 验证密码 54 | console.log("开始验证密码"); 55 | const isPasswordValid = await bcrypt.compare(password, user.password); 56 | console.log("密码验证结果:", isPasswordValid); 57 | 58 | if (!isPasswordValid) { 59 | console.log("密码验证失败"); 60 | return { success: false, message: "用户名或密码错误" }; 61 | } 62 | 63 | // 生成JWT令牌 64 | const payload = { 65 | id: user.id, 66 | username: user.username, 67 | role: user.role, 68 | }; 69 | console.log("JWT payload:", payload); 70 | 71 | console.log("调用 getJwtSecret 前的环境变量检查:"); 72 | console.log("env 是否存在:", !!env); 73 | console.log("env.CF_VERSION_METADATA 是否存在:", !!env.CF_VERSION_METADATA); 74 | 75 | try { 76 | const secret = getJwtSecret(env); 77 | console.log("获取到的 JWT secret:", secret); 78 | 79 | const token = jsonwebtoken.sign(payload, secret, { expiresIn: "24h" }); 80 | console.log("成功生成 JWT token, 长度:", token.length); 81 | 82 | return { 83 | success: true, 84 | message: "登录成功", 85 | token, 86 | user: { id: user.id, username: user.username, role: user.role }, 87 | }; 88 | } catch (jwtError) { 89 | console.error("JWT 生成错误:", jwtError); 90 | console.error( 91 | "JWT 错误堆栈:", 92 | jwtError instanceof Error ? jwtError.stack : "未知错误" 93 | ); 94 | return { success: false, message: "Token 生成失败" }; 95 | } 96 | } catch (error) { 97 | console.error("登录错误:", error); 98 | console.error( 99 | "错误堆栈:", 100 | error instanceof Error ? error.stack : "未知错误" 101 | ); 102 | return { success: false, message: "登录处理失败" }; 103 | } 104 | } 105 | 106 | /** 107 | * 用户注册 108 | * @param env 环境变量,包含数据库连接 109 | * @param username 用户名 110 | * @param password 密码(明文) 111 | * @param email 电子邮箱 112 | * @param role 用户角色 113 | * @returns 注册结果 114 | */ 115 | export async function registerUser( 116 | env: { DB: Bindings["DB"] } & any, 117 | username: string, 118 | password: string, 119 | email: string | null = null, 120 | role: string = "user" 121 | ): Promise<{ success: boolean; message: string; user?: any }> { 122 | try { 123 | // 检查用户名是否已存在 124 | const existingUser = await repositories.getUserByUsername(username); 125 | if (existingUser) { 126 | return { success: false, message: "用户名已存在" }; 127 | } 128 | 129 | // 密码加密 130 | const salt = await bcrypt.genSalt(10); 131 | const hashedPassword = await bcrypt.hash(password, salt); 132 | 133 | // 创建用户 134 | const newUser = await repositories.createUser( 135 | username, 136 | hashedPassword, 137 | email, 138 | role 139 | ); 140 | 141 | return { 142 | success: true, 143 | message: "注册成功", 144 | user: newUser, 145 | }; 146 | } catch (error) { 147 | console.error("注册错误:", error); 148 | return { success: false, message: "注册处理失败" }; 149 | } 150 | } 151 | 152 | /** 153 | * 获取当前用户信息 154 | * @param env 环境变量,包含数据库连接 155 | * @param userId 用户ID 156 | * @returns 用户信息 157 | */ 158 | export async function getCurrentUser( 159 | env: { DB: Bindings["DB"] } & any, 160 | userId: number 161 | ): Promise<{ success: boolean; message: string; user?: any }> { 162 | try { 163 | const user = await repositories.getUserById(userId); 164 | 165 | if (!user) { 166 | return { success: false, message: "用户不存在" }; 167 | } 168 | 169 | return { 170 | success: true, 171 | message: "获取用户信息成功", 172 | user, 173 | }; 174 | } catch (error) { 175 | console.error("获取用户信息错误:", error); 176 | return { success: false, message: "获取用户信息失败" }; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /backend/src/services/DashboardService.ts: -------------------------------------------------------------------------------- 1 | import { Monitor } from "../models"; 2 | import * as repositories from "../repositories"; 3 | 4 | export async function getDashboardData() { 5 | const monitors = await repositories.getAllMonitors(); 6 | const agents = await repositories.getAllAgents(); 7 | 8 | if (monitors.monitors && monitors.monitors.length > 0) { 9 | monitors.monitors.forEach((monitor: Monitor) => { 10 | if (typeof monitor.headers === "string") { 11 | try { 12 | monitor.headers = JSON.parse(monitor.headers); 13 | } catch (e) { 14 | monitor.headers = {}; 15 | } 16 | } 17 | }); 18 | } 19 | return { 20 | monitors: monitors.monitors, 21 | agents: agents, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MonitorService'; 2 | export * from './AgentService'; 3 | export * from './AuthService'; 4 | export * from './NotificationService'; 5 | export * from './StatusService'; 6 | export * from './UserService'; -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "CommonJS", 5 | "moduleResolution": "Node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "outDir": "dist", 10 | "sourceMap": true, 11 | "declaration": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } -------------------------------------------------------------------------------- /backend/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "xugou-backend" 2 | main = "src/index.ts" 3 | compatibility_date = "2025-03-10" 4 | compatibility_flags = ["nodejs_compat"] 5 | 6 | # 添加定时触发器,每分钟执行一次监控检查 7 | [triggers] 8 | crons = ["* * * * *"] 9 | 10 | [[d1_databases]] 11 | binding = "DB" 12 | database_name = "xugou_db" 13 | database_id = "32a2ca5f-3a77-4b08-965e-804612557880" 14 | 15 | [version_metadata] 16 | binding = "CF_VERSION_METADATA" -------------------------------------------------------------------------------- /frontend/.env.local.example: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL=http://localhost:8787 -------------------------------------------------------------------------------- /frontend/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/global.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | XUGOU - 轻量化监控平台 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 14 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 15 | "@fortawesome/react-fontawesome": "^0.2.2", 16 | "@radix-ui/react-alert-dialog": "^1.1.13", 17 | "@radix-ui/react-avatar": "^1.1.9", 18 | "@radix-ui/react-checkbox": "^1.3.1", 19 | "@radix-ui/react-dialog": "^1.1.13", 20 | "@radix-ui/react-dropdown-menu": "^2.1.14", 21 | "@radix-ui/react-icons": "^1.3.2", 22 | "@radix-ui/react-select": "^2.2.4", 23 | "@radix-ui/react-separator": "^1.1.6", 24 | "@radix-ui/react-slot": "^1.2.2", 25 | "@radix-ui/react-switch": "^1.2.4", 26 | "@radix-ui/react-tabs": "^1.1.11", 27 | "@radix-ui/react-tooltip": "^1.2.6", 28 | "@radix-ui/themes": "^2.0.3", 29 | "@tailwindcss/vite": "^4.1.7", 30 | "axios": "^1.9.0", 31 | "chart.js": "^4.4.9", 32 | "chartjs-adapter-moment": "^1.0.1", 33 | "class-variance-authority": "^0.7.1", 34 | "clsx": "^2.1.1", 35 | "i18next": "^24.2.3", 36 | "i18next-browser-languagedetector": "^8.1.0", 37 | "lucide-react": "^0.511.0", 38 | "next-themes": "^0.4.6", 39 | "react": "^18.3.1", 40 | "react-chartjs-2": "^5.3.0", 41 | "react-dom": "^18.3.1", 42 | "react-i18next": "^15.5.1", 43 | "react-router-dom": "^6.30.0", 44 | "shadcn": "^2.5.0", 45 | "sonner": "^2.0.3", 46 | "tailwind-merge": "^3.3.0", 47 | "tailwindcss": "^4.1.7", 48 | "zod": "^3.25.7" 49 | }, 50 | "devDependencies": { 51 | "@cloudflare/workers-types": "^4.20250520.0", 52 | "@types/node": "^22.15.19", 53 | "@types/react": "^18.3.21", 54 | "@types/react-dom": "^18.3.7", 55 | "@typescript-eslint/eslint-plugin": "^6.21.0", 56 | "@typescript-eslint/parser": "^6.21.0", 57 | "@vitejs/plugin-react": "^4.4.1", 58 | "eslint": "^8.57.1", 59 | "eslint-plugin-react-hooks": "^4.6.2", 60 | "eslint-plugin-react-refresh": "^0.4.20", 61 | "globals": "^13.24.0", 62 | "tw-animate-css": "^1.3.0", 63 | "typescript": "~5.1.6", 64 | "vite": "^6.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaunist/xugou/affe220bf5351188c8e043e635a0934b5181dc55/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaunist/xugou/affe220bf5351188c8e043e635a0934b5181dc55/frontend/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaunist/xugou/affe220bf5351188c8e043e635a0934b5181dc55/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaunist/xugou/affe220bf5351188c8e043e635a0934b5181dc55/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaunist/xugou/affe220bf5351188c8e043e635a0934b5181dc55/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XUGOU", 3 | "short_name": "XUGOU", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "/android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png", 19 | "purpose": "maskable" 20 | } 21 | ], 22 | "theme_color": "#4F46E5", 23 | "background_color": "#ffffff", 24 | "display": "standalone", 25 | "id": "67920b7d-8a9d-4a86-aa81-7eabc04b1e53", 26 | "description": "XUGOU 是一个基于 CloudFlare 的轻量化系统监控平台,提供系统监控和状态页面功能。", 27 | "start_url": "/", 28 | "dir": "auto", 29 | "lang": "zh", 30 | "scope": "/", 31 | "orientation": "portrait", 32 | "iarc_rating_id": "e58c174a-81d2-5c3c-32cc-34b8de4a52e9", 33 | "prefer_related_applications": false 34 | } -------------------------------------------------------------------------------- /frontend/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "XUGOU", 3 | "short_name": "XUGOU", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#4F46E5", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /frontend/public/sw.js: -------------------------------------------------------------------------------- 1 | self.addEventListener("install", (event) => { 2 | self.skipWaiting(); 3 | }); 4 | 5 | self.addEventListener("activate", (event) => { 6 | self.clients.claim(); 7 | }); 8 | 9 | self.addEventListener("fetch", (event) => { 10 | event.respondWith(fetch(event.request)); 11 | }); -------------------------------------------------------------------------------- /frontend/src/api/agents.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { 3 | Agent, 4 | AgentResponse, 5 | AgentsResponse, 6 | MetricHistory, 7 | } from "../types/agents"; 8 | 9 | export const generateToken = async (): Promise<{ 10 | success: boolean; 11 | token?: string; 12 | message?: string; 13 | }> => { 14 | try { 15 | const response = await api.post("/api/agents/token/generate"); 16 | return response.data; 17 | } catch (error) { 18 | console.error("生成客户端注册令牌失败:", error); 19 | return { 20 | success: false, 21 | message: "生成客户端注册令牌失败", 22 | }; 23 | } 24 | }; 25 | 26 | export const getAllAgents = async (): Promise => { 27 | const response = await api.get("/api/agents"); 28 | return { 29 | success: true, 30 | agents: response.data.agents, 31 | }; 32 | }; 33 | 34 | export const getAgent = async (id: number): Promise => { 35 | const response = await api.get(`/api/agents/${id}`); 36 | return { 37 | success: true, 38 | agent: response.data.agent, 39 | }; 40 | }; 41 | 42 | export const deleteAgent = async ( 43 | id: number 44 | ): Promise<{ success: boolean; message: string }> => { 45 | try { 46 | const response = await api.delete(`/api/agents/${id}`); 47 | return response.data; 48 | } catch (error) { 49 | console.error(`删除客户端 ${id} 失败:`, error); 50 | return { 51 | success: false, 52 | message: "删除客户端失败", 53 | }; 54 | } 55 | }; 56 | 57 | export const updateAgent = async ( 58 | id: number, 59 | data: Partial 60 | ): Promise => { 61 | try { 62 | const response = await api.put(`/api/agents/${id}`, data); 63 | return response.data; 64 | } catch (error) { 65 | console.error(`更新客户端 ${id} 失败:`, error); 66 | return { 67 | success: false, 68 | }; 69 | } 70 | }; 71 | 72 | export const getAgentMetrics = async ( 73 | id: number 74 | ): Promise<{ 75 | success: boolean; 76 | agent?: MetricHistory[]; 77 | message?: string; 78 | }> => { 79 | try { 80 | const response = await api.get(`/api/agents/${id}/metrics`); 81 | return response.data; 82 | } catch (error) { 83 | console.error(`获取客户端 ${id} 的指标失败:`, error); 84 | return { 85 | success: false, 86 | message: "获取客户端指标失败", 87 | }; 88 | } 89 | }; 90 | 91 | export const getLatestAgentMetrics = async ( 92 | id: number 93 | ): Promise<{ 94 | success: boolean; 95 | agent?: MetricHistory; 96 | message?: string; 97 | }> => { 98 | const response = await api.get(`/api/agents/${id}/metrics/latest`); 99 | return response.data; 100 | }; 101 | -------------------------------------------------------------------------------- /frontend/src/api/auth.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { LoginRequest, AuthResponse } from "../types/auth"; 3 | 4 | // 登录 5 | export const login = async ( 6 | credentials: LoginRequest 7 | ): Promise => { 8 | const response = await api.post("/api/auth/login", credentials); 9 | return response.data; 10 | }; 11 | 12 | // 注册 13 | export const register = async (data: any): Promise => { 14 | const response = await api.post("/api/auth/register", data); 15 | return response.data; 16 | }; 17 | 18 | // 获取当前用户信息 19 | export const getCurrentUser = async (): Promise => { 20 | const response = await api.get("/api/auth/me"); 21 | return response.data; 22 | }; 23 | 24 | // 退出登录 25 | export const logout = (): void => { 26 | localStorage.removeItem("token"); 27 | localStorage.removeItem("user"); 28 | window.location.href = "/login"; 29 | }; 30 | -------------------------------------------------------------------------------- /frontend/src/api/client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { ENV_API_BASE_URL, ENV_API_TIMEOUT } from "../config"; 3 | 4 | // 创建 axios 实例 5 | const api = axios.create({ 6 | baseURL: ENV_API_BASE_URL, // 从配置中获取API基础URL 7 | timeout: ENV_API_TIMEOUT, // 从配置中获取超时设置 8 | headers: { 9 | "Content-Type": "application/json", 10 | }, 11 | // Cloudflare Pages 访问 Cloudflare Workers 时的跨域设置 12 | withCredentials: false, // 不发送 cookies 13 | }); 14 | 15 | // 请求拦截器 16 | api.interceptors.request.use( 17 | (config) => { 18 | // 从 localStorage 获取 token 19 | const token = localStorage.getItem("token"); 20 | if (token) { 21 | config.headers.Authorization = `Bearer ${token}`; 22 | } 23 | return config; 24 | }, 25 | (error) => { 26 | return Promise.reject(error); 27 | } 28 | ); 29 | 30 | // 响应拦截器 31 | api.interceptors.response.use( 32 | (response) => { 33 | return response; 34 | }, 35 | (error) => { 36 | if (error.response) { 37 | // 处理 401 未授权错误 38 | if (error.response.status === 401) { 39 | localStorage.removeItem("token"); 40 | localStorage.removeItem("user"); 41 | window.location.href = "/login"; 42 | } 43 | } 44 | return Promise.reject(error); 45 | } 46 | ); 47 | 48 | export default api; 49 | -------------------------------------------------------------------------------- /frontend/src/api/dashboard.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { Monitor, Agent } from "../types"; 3 | 4 | // 获取仪表盘数据 5 | export const getDashboardData = async (): Promise<{ 6 | monitors: Monitor[]; 7 | agents: Agent[]; 8 | }> => { 9 | const response = await api.get("/api/dashboard"); 10 | return response.data; 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./agents"; 2 | export * from "./auth"; 3 | export * from "./client"; 4 | export * from "./dashboard"; 5 | export * from "./monitors"; 6 | export * from "./notifications"; 7 | export * from "./status"; 8 | export * from "./users"; -------------------------------------------------------------------------------- /frontend/src/api/monitors.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { 3 | MonitorResponse, 4 | MonitorsResponse, 5 | CreateMonitorRequest, 6 | UpdateMonitorRequest, 7 | MonitorStatusHistoryResponse, 8 | DailyStatsResponse, 9 | } from "../types/monitors"; 10 | 11 | // 获取所有监控 12 | export const getAllMonitors = async (): Promise => { 13 | const response = await api.get("/api/monitors"); 14 | return response.data; 15 | }; 16 | 17 | // 获取所有每日统计 18 | export const getAllDailyStats = async (): Promise => { 19 | const response = await api.get("/api/monitors/daily"); 20 | return response.data; 21 | }; 22 | 23 | // 获取单个监控每日统计 24 | export const getMonitorDailyStats = async (id: number): Promise => { 25 | const response = await api.get(`/api/monitors/${id}/daily`); 26 | return response.data; 27 | }; 28 | 29 | // 获取单个监控 30 | export const getMonitor = async (id: number): Promise => { 31 | const response = await api.get(`/api/monitors/${id}`); 32 | return response.data; 33 | }; 34 | 35 | // 创建监控 36 | export const createMonitor = async ( 37 | data: CreateMonitorRequest 38 | ): Promise => { 39 | const response = await api.post("/api/monitors", data); 40 | return response.data; 41 | }; 42 | 43 | // 更新监控 44 | export const updateMonitor = async ( 45 | id: number, 46 | data: UpdateMonitorRequest 47 | ): Promise => { 48 | const response = await api.put(`/api/monitors/${id}`, data); 49 | return response.data; 50 | }; 51 | 52 | // 删除监控 53 | export const deleteMonitor = async (id: number): Promise => { 54 | const response = await api.delete(`/api/monitors/${id}`); 55 | return response.data; 56 | }; 57 | 58 | // 获取单个监控历史 24小时内 59 | export const getMonitorStatusHistoryById = async ( 60 | id: number 61 | ): Promise => { 62 | const response = await api.get( 63 | `/api/monitors/${id}/history` 64 | ); 65 | return response.data; 66 | }; 67 | 68 | // 获取所有监控历史 24小时内 69 | export const getAllMonitorHistory = 70 | async (): Promise => { 71 | const response = await api.get( 72 | `/api/monitors/history` 73 | ); 74 | return response.data; 75 | }; 76 | 77 | // 手动检查监控 78 | export const checkMonitor = async ( 79 | id: number 80 | ): Promise => { 81 | const response = await api.post( 82 | `/api/monitors/${id}/check` 83 | ); 84 | return response.data; 85 | }; 86 | -------------------------------------------------------------------------------- /frontend/src/api/status.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { 3 | StatusPageConfig, 4 | StatusPageConfigResponse, 5 | StatusPageData, 6 | } from "../types/status"; 7 | 8 | // 获取状态页配置 9 | export const getStatusPageConfig = 10 | async (): Promise => { 11 | const response = await api.get( 12 | "/api/status/config" 13 | ); 14 | return response.data; 15 | }; 16 | 17 | // 保存状态页配置 18 | export const saveStatusPageConfig = async ( 19 | config: StatusPageConfig 20 | ): Promise => { 21 | const response = await api.post( 22 | "/api/status/config", 23 | config 24 | ); 25 | return response.data; 26 | }; 27 | 28 | // 获取状态页数据 29 | export const getStatusPageData = async (): Promise => { 30 | const response = await api.get("/api/status/data"); 31 | return response.data; 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/api/users.ts: -------------------------------------------------------------------------------- 1 | import api from "./client"; 2 | import { 3 | User, 4 | UserResponse, 5 | CreateUserRequest, 6 | UpdateUserRequest, 7 | ChangePasswordRequest, 8 | } from "../types/users"; 9 | 10 | // 获取所有用户 11 | export const getAllUsers = async (): Promise<{ 12 | success: boolean; 13 | message?: string; 14 | users?: User[]; 15 | }> => { 16 | const response = await api.get("/api/users"); 17 | return response.data; 18 | }; 19 | 20 | // 获取单个用户 21 | export const getUser = async (id: number): Promise => { 22 | const response = await api.get(`/api/users/${id}`); 23 | return response.data; 24 | }; 25 | 26 | // 创建用户 27 | export const createUser = async ( 28 | data: CreateUserRequest 29 | ): Promise => { 30 | const response = await api.post("/api/users", data); 31 | return response.data; 32 | }; 33 | 34 | // 更新用户 35 | export const updateUser = async ( 36 | id: number, 37 | data: UpdateUserRequest 38 | ): Promise => { 39 | const response = await api.put(`/api/users/${id}`, data); 40 | return response.data; 41 | }; 42 | 43 | // 删除用户 44 | export const deleteUser = async ( 45 | id: number 46 | ): Promise<{ success: boolean; message?: string }> => { 47 | const response = await api.delete(`/api/users/${id}`); 48 | return response.data; 49 | }; 50 | 51 | // 修改密码 52 | export const changePassword = async ( 53 | id: number, 54 | data: ChangePasswordRequest 55 | ): Promise<{ success: boolean; message?: string }> => { 56 | const response = await api.post(`/api/users/${id}/change-password`, data); 57 | return response.data; 58 | }; 59 | -------------------------------------------------------------------------------- /frontend/src/components/AgentCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Card, Flex, Text } from "@radix-ui/themes"; 2 | import { Tabs, TabsContent, TabsList, TabsTrigger, Badge } from "./ui"; 3 | import { GlobeIcon } from "@radix-ui/react-icons"; 4 | import MetricsChart from "./MetricsChart"; 5 | import { useTranslation } from "react-i18next"; 6 | import { AgentCardProps, MetricType } from "../types"; 7 | 8 | /** 9 | * 客户端状态卡片组件 10 | * 用于显示单个客户端的状态和资源使用情况 11 | */ 12 | const AgentCard = ({ 13 | agent, 14 | showIpAddress = true, 15 | showHostname = true, 16 | showLastUpdated = true, 17 | }: AgentCardProps) => { 18 | const { t } = useTranslation(); 19 | 20 | console.log(t("agentCard.receivedData"), agent); 21 | 22 | // 根据status属性判断状态 23 | const agentStatus = agent.status || "inactive"; 24 | 25 | // 状态颜色映射 26 | const statusColors: { [key: string]: string } = { 27 | active: "green", 28 | inactive: "amber", 29 | connecting: "yellow", 30 | }; 31 | 32 | // 状态文本映射 33 | const statusText: { [key: string]: string } = { 34 | active: t("agentCard.status.active"), 35 | inactive: t("agentCard.status.inactive"), 36 | connecting: t("agentCard.status.connecting"), 37 | }; 38 | 39 | // 获取IP地址显示的文本 40 | const getIpAddressText = () => { 41 | if (!agent.ip_addresses) return t("common.notFound"); 42 | const ipArray = JSON.parse(String(agent.ip_addresses)); 43 | return Array.isArray(ipArray) && ipArray.length > 0 44 | ? ipArray[0] + (ipArray.length > 1 ? ` (+${ipArray.length - 1})` : "") 45 | : String(agent.ip_addresses); 46 | }; 47 | 48 | // 格式化最后更新时间 49 | const formatLastUpdated = () => { 50 | if (!agent.updated_at) return ""; 51 | 52 | try { 53 | return new Date(agent.updated_at).toLocaleString(); 54 | } catch (e) { 55 | return agent.updated_at; 56 | } 57 | }; 58 | 59 | // 定义所有可用的指标类型 60 | const metricTypes: MetricType[] = [ 61 | "cpu", 62 | "memory", 63 | "disk", 64 | "network", 65 | "load", 66 | ]; 67 | 68 | return ( 69 | 70 | 71 | 72 | 73 | 79 | 80 | 81 | {agent.name} 82 | 83 | 84 | {showHostname && agent.hostname && ( 85 | 86 | {agent.hostname} 87 | 88 | )} 89 | 90 | {showIpAddress && agent.ip_addresses && ( 91 | 92 | {getIpAddressText()} 93 | 94 | )} 95 | 96 | {showLastUpdated && agent.updated_at && ( 97 | 98 | {t("agent.lastUpdated")}: {formatLastUpdated()} 99 | 100 | )} 101 | 102 | {statusText[agentStatus] || agentStatus} 103 | 104 | 105 | 106 | 107 | {/* 指标图表区域 */} 108 | 109 | 110 | {metricTypes.map((type) => ( 111 | 112 | {t(`agent.metrics.${type}.title`) || type.toUpperCase()} 113 | 114 | ))} 115 | 116 | 117 | {metricTypes.map((type) => ( 118 | 119 | 127 | 128 | ))} 129 | 130 | 131 | 132 | ); 133 | }; 134 | 135 | export default AgentCard; 136 | -------------------------------------------------------------------------------- /frontend/src/components/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, Flex } from "@radix-ui/themes"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuTrigger, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | } from "./ui"; 9 | import { GlobeIcon, CheckIcon } from "@radix-ui/react-icons"; 10 | import { useLanguage } from "../providers/LanguageProvider"; 11 | import { useTranslation } from "react-i18next"; 12 | 13 | const LanguageSelector: React.FC = () => { 14 | const { currentLanguage, changeLanguage, availableLanguages } = useLanguage(); 15 | const { t } = useTranslation(); 16 | 17 | return ( 18 | 19 | 20 | 25 | 26 | 27 | {currentLanguage === "zh-CN" ? "中文" : "English"} 28 | 29 | 30 | 31 | 32 | {availableLanguages.map((lang) => ( 33 | changeLanguage(lang.code)} 36 | > 37 | 43 | 44 | {t(`language.${lang.code.replace("-", "")}`)} 45 | 46 | {currentLanguage === lang.code && ( 47 | 48 | )} 49 | 50 | 51 | ))} 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default LanguageSelector; 58 | -------------------------------------------------------------------------------- /frontend/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { Box, Flex, Text, Container, Theme } from "@radix-ui/themes"; 3 | import { Separator, Button, Toaster } from "./ui"; 4 | import Navbar from "./Navbar"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faRss } from "@fortawesome/free-solid-svg-icons"; 7 | import { faYoutube } from "@fortawesome/free-brands-svg-icons"; 8 | import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; 9 | import { useTranslation } from "react-i18next"; 10 | 11 | interface LayoutProps { 12 | children: ReactNode; 13 | } 14 | 15 | const Layout = ({ children }: LayoutProps) => { 16 | const currentYear = new Date().getFullYear(); 17 | const { t } = useTranslation(); 18 | 19 | return ( 20 | 21 | 22 | {/* 顶部导航栏 */} 23 | 24 | 25 | {/* 主要内容 */} 26 | {children} 27 | 28 | {/* 页脚 */} 29 | 30 | 31 | 32 | 33 | 34 | {t("footer.copyright", { year: currentYear })} 35 | 36 | 37 | 52 | 67 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | export default Layout; 93 | -------------------------------------------------------------------------------- /frontend/src/components/MonitorCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Text } from "@radix-ui/themes"; 2 | import { 3 | CheckCircledIcon, 4 | CrossCircledIcon, 5 | QuestionMarkCircledIcon, 6 | } from "@radix-ui/react-icons"; 7 | import { Card, Badge } from "./ui"; 8 | import { MonitorWithDailyStatsAndStatusHistory } from "../types/monitors"; 9 | import { useTranslation } from "react-i18next"; 10 | import StatusBar from "./MonitorStatusBar"; 11 | import ResponseTimeChart from "./ResponseTimeChart"; 12 | 13 | interface MonitorCardProps { 14 | monitor: MonitorWithDailyStatsAndStatusHistory; 15 | } 16 | 17 | /** 18 | * API监控卡片组件 19 | * 用于显示单个API监控服务的状态信息 20 | */ 21 | const MonitorCard = ({ monitor }: MonitorCardProps) => { 22 | const { t } = useTranslation(); 23 | 24 | // 状态图标组件 25 | const StatusIcon = ({ status }: { status: string }) => { 26 | switch (status) { 27 | case "up": 28 | return ( 29 | 30 | ); 31 | case "pending": 32 | return ( 33 | 38 | ); 39 | case "down": 40 | default: 41 | return ; 42 | } 43 | }; 44 | 45 | // 状态颜色映射 46 | const statusColors: { [key: string]: string } = { 47 | up: "green", 48 | down: "red", 49 | pending: "amber", 50 | }; 51 | 52 | // 状态文本映射 53 | const statusText: { [key: string]: string } = { 54 | up: t("monitorCard.status.up"), 55 | down: t("monitorCard.status.down"), 56 | pending: t("monitorCard.status.pending"), 57 | }; 58 | 59 | // 获取当前监控的状态 60 | const currentStatus = monitor.status || "pending"; 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | 68 | {monitor.name} 69 | 70 | 71 | {statusText[currentStatus]} 72 | 73 | 74 | 75 | {/* 状态条显示 */} 76 | 77 | 78 | 79 | 80 | {/* 响应时间图表 */} 81 | 82 | 87 | 88 | 89 | 90 | ); 91 | }; 92 | 93 | export default MonitorCard; 94 | -------------------------------------------------------------------------------- /frontend/src/components/StatusCodeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from "@radix-ui/themes"; 2 | import { 3 | Select, 4 | SelectContent, 5 | SelectItem, 6 | SelectLabel, 7 | SelectTrigger, 8 | SelectSeparator, 9 | SelectValue, 10 | SelectGroup, 11 | } from "./ui"; 12 | 13 | // 状态码选项定义 14 | export const specificStatusCodes = [ 15 | { 16 | group: "2xx - 成功", 17 | codes: [ 18 | { label: "2xx - 所有成功状态码", value: 2, isRange: true }, 19 | { label: "200 - OK", value: 200 }, 20 | { label: "201 - Created", value: 201 }, 21 | { label: "204 - No Content", value: 204 }, 22 | ], 23 | }, 24 | { 25 | group: "3xx - 重定向", 26 | codes: [ 27 | { label: "3xx - 所有重定向状态码", value: 3, isRange: true }, 28 | { label: "301 - Moved Permanently", value: 301 }, 29 | { label: "302 - Found", value: 302 }, 30 | { label: "304 - Not Modified", value: 304 }, 31 | ], 32 | }, 33 | { 34 | group: "4xx - 客户端错误", 35 | codes: [ 36 | { label: "4xx - 所有客户端错误状态码", value: 4, isRange: true }, 37 | { label: "400 - Bad Request", value: 400 }, 38 | { label: "401 - Unauthorized", value: 401 }, 39 | { label: "403 - Forbidden", value: 403 }, 40 | { label: "404 - Not Found", value: 404 }, 41 | ], 42 | }, 43 | { 44 | group: "5xx - 服务器错误", 45 | codes: [ 46 | { label: "5xx - 所有服务器错误状态码", value: 5, isRange: true }, 47 | { label: "500 - Internal Server Error", value: 500 }, 48 | { label: "502 - Bad Gateway", value: 502 }, 49 | { label: "503 - Service Unavailable", value: 503 }, 50 | { label: "504 - Gateway Timeout", value: 504 }, 51 | ], 52 | }, 53 | ]; 54 | 55 | interface StatusCodeSelectProps { 56 | value: number | string; 57 | onChange: (value: number) => void; 58 | name?: string; 59 | required?: boolean; 60 | } 61 | 62 | /** 63 | * 预期状态码选择组件 64 | * 提供HTTP状态码和状态码范围的选择功能 65 | */ 66 | const StatusCodeSelect = ({ 67 | value, 68 | onChange, 69 | name = "expectedStatus", 70 | required = false, 71 | }: StatusCodeSelectProps) => { 72 | return ( 73 | <> 74 | 100 | 101 | 选择预期的HTTP状态码或状态码范围 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default StatusCodeSelect; 108 | -------------------------------------------------------------------------------- /frontend/src/components/StatusSummaryCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Heading, Text } from "@radix-ui/themes"; 2 | import { Card } from "./ui"; 3 | import { ReactNode } from "react"; 4 | 5 | interface StatusItem { 6 | icon: ReactNode; 7 | label: string; 8 | value: number; 9 | bgColor?: string; 10 | iconColor?: string; 11 | } 12 | 13 | interface StatusSummaryCardProps { 14 | title: string; 15 | items: StatusItem[]; 16 | } 17 | 18 | /** 19 | * 状态摘要卡片组件 20 | * 用于显示系统状态概览,如正常/异常服务数量等 21 | */ 22 | const StatusSummaryCard = ({ title, items }: StatusSummaryCardProps) => { 23 | return ( 24 | 25 | 26 | 27 | {title} 28 | 29 | 30 | {items.map((item, index) => ( 31 | 32 | 33 | 34 | {item.icon} 35 | 36 | {item.label} 37 | 38 | {item.value} 39 | 40 | ))} 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default StatusSummaryCard; 48 | -------------------------------------------------------------------------------- /frontend/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /frontend/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | color: { 22 | default: "", 23 | green: 24 | "border-transparent bg-green-500/15 text-green-700 dark:text-green-400", 25 | red: "border-transparent bg-red-500/15 text-red-700 dark:text-red-400", 26 | yellow: 27 | "border-transparent bg-yellow-500/15 text-yellow-700 dark:text-yellow-400", 28 | amber: 29 | "border-transparent bg-amber-500/15 text-amber-700 dark:text-amber-400", 30 | gray: "border-transparent bg-gray-500/15 text-gray-700 dark:text-gray-400", 31 | }, 32 | }, 33 | defaultVariants: { 34 | variant: "default", 35 | color: "default", 36 | }, 37 | } 38 | ); 39 | 40 | function Badge({ 41 | className, 42 | variant, 43 | color, 44 | asChild = false, 45 | ...props 46 | }: React.ComponentProps<"span"> & 47 | VariantProps & { asChild?: boolean; color?: string }) { 48 | const Comp = asChild ? Slot : "span"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Badge, badgeVariants }; 60 | -------------------------------------------------------------------------------- /frontend/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button" 47 | return ( 48 | 53 | ) 54 | } 55 | ) 56 | Button.displayName = "Button" 57 | 58 | export { Button, buttonVariants } 59 | -------------------------------------------------------------------------------- /frontend/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /frontend/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export { Checkbox } 33 | -------------------------------------------------------------------------------- /frontend/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DialogPrimitive from "@radix-ui/react-dialog" 3 | import { XIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Dialog = DialogPrimitive.Root 8 | 9 | const DialogTrigger = DialogPrimitive.Trigger 10 | 11 | const DialogPortal = DialogPrimitive.Portal 12 | 13 | const DialogClose = DialogPrimitive.Close 14 | 15 | const DialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )) 28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 29 | 30 | const DialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, children, ...props }, ref) => ( 34 | 35 | 36 | 44 | {children} 45 | 46 | 47 | Close 48 | 49 | 50 | 51 | )) 52 | DialogContent.displayName = DialogPrimitive.Content.displayName 53 | 54 | const DialogHeader = ({ 55 | className, 56 | ...props 57 | }: React.HTMLAttributes) => ( 58 |
65 | ) 66 | DialogHeader.displayName = "DialogHeader" 67 | 68 | const DialogFooter = ({ 69 | className, 70 | ...props 71 | }: React.HTMLAttributes) => ( 72 |
79 | ) 80 | DialogFooter.displayName = "DialogFooter" 81 | 82 | const DialogTitle = React.forwardRef< 83 | React.ElementRef, 84 | React.ComponentPropsWithoutRef 85 | >(({ className, ...props }, ref) => ( 86 | 94 | )) 95 | DialogTitle.displayName = DialogPrimitive.Title.displayName 96 | 97 | const DialogDescription = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 106 | )) 107 | DialogDescription.displayName = DialogPrimitive.Description.displayName 108 | 109 | export { 110 | Dialog, 111 | DialogPortal, 112 | DialogOverlay, 113 | DialogTrigger, 114 | DialogClose, 115 | DialogContent, 116 | DialogHeader, 117 | DialogFooter, 118 | DialogTitle, 119 | DialogDescription, 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/components/ui/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./card" 2 | export * from "./textarea" 3 | export * from "./badge" 4 | export * from "./button" 5 | export * from "./tabs" 6 | export * from "./avatar" 7 | export * from "./dropdown-menu" 8 | export * from "./separator" 9 | export * from "./select" 10 | export * from "./tooltip" 11 | export * from "./dialog" 12 | export * from "./switch" 13 | export * from "./alert-dialog" 14 | export * from "./textarea" 15 | export * from "./table" 16 | export * from "./checkbox" 17 | export * from "./sonner" 18 | export * from "./input" -------------------------------------------------------------------------------- /frontend/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /frontend/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /frontend/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } -------------------------------------------------------------------------------- /frontend/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitive from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ) 29 | } 30 | 31 | export { Switch } 32 | -------------------------------------------------------------------------------- /frontend/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Table({ className, ...props }: React.ComponentProps<"table">) { 6 | return ( 7 |
11 | 16 | 17 | ) 18 | } 19 | 20 | function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { 21 | return ( 22 | 27 | ) 28 | } 29 | 30 | function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { 31 | return ( 32 | 37 | ) 38 | } 39 | 40 | function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { 41 | return ( 42 | tr]:last:border-b-0", 46 | className 47 | )} 48 | {...props} 49 | /> 50 | ) 51 | } 52 | 53 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) { 54 | return ( 55 | 63 | ) 64 | } 65 | 66 | function TableHead({ className, ...props }: React.ComponentProps<"th">) { 67 | return ( 68 |
[role=checkbox]]:translate-y-[2px]", 72 | className 73 | )} 74 | {...props} 75 | /> 76 | ) 77 | } 78 | 79 | function TableCell({ className, ...props }: React.ComponentProps<"td">) { 80 | return ( 81 | [role=checkbox]]:translate-y-[2px]", 85 | className 86 | )} 87 | {...props} 88 | /> 89 | ) 90 | } 91 | 92 | function TableCaption({ 93 | className, 94 | ...props 95 | }: React.ComponentProps<"caption">) { 96 | return ( 97 |
102 | ) 103 | } 104 | 105 | export { 106 | Table, 107 | TableHeader, 108 | TableBody, 109 | TableFooter, 110 | TableHead, 111 | TableRow, 112 | TableCell, 113 | TableCaption, 114 | } 115 | -------------------------------------------------------------------------------- /frontend/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TabsPrimitive from "@radix-ui/react-tabs" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function TabsList({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 37 | ) 38 | } 39 | 40 | function TabsTrigger({ 41 | className, 42 | ...props 43 | }: React.ComponentProps) { 44 | return ( 45 | 71 | ) 72 | } 73 | 74 | function TabsContent({ 75 | className, 76 | ...props 77 | }: React.ComponentProps) { 78 | return ( 79 | 90 | ) 91 | } 92 | 93 | export { Tabs, TabsList, TabsTrigger, TabsContent } -------------------------------------------------------------------------------- /frontend/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |