├── web ├── .env.production ├── .vscode │ └── extensions.json ├── .env.development ├── public │ └── favicon.ico ├── tsconfig.json ├── src │ ├── locales │ │ ├── types.ts │ │ ├── index.ts │ │ ├── zh-CN.ts │ │ └── en-US.ts │ ├── stores │ │ ├── index.ts │ │ ├── server.ts │ │ └── connection.ts │ ├── vite-env.d.ts │ ├── utils │ │ ├── CommonUtil.ts │ │ ├── colorUtils.ts │ │ └── formatters.ts │ ├── main.ts │ ├── components │ │ ├── FlagIcon.vue │ │ ├── Logo.vue │ │ ├── StatusIndicator.vue │ │ └── ServerInfoContent.vue │ ├── services │ │ └── serverService.ts │ ├── api │ │ ├── models │ │ │ └── index.ts │ │ ├── helper │ │ │ └── checkStatus.ts │ │ └── index.ts │ ├── composables │ │ ├── useErrorHandler.ts │ │ ├── useServerInfoFormatting.ts │ │ └── useConnectionManager.ts │ ├── constants │ │ └── connectionModes.ts │ ├── types │ │ ├── api.ts │ │ └── websocket.ts │ └── pages │ │ └── StatusPage.vue ├── .gitignore ├── index.html ├── README.md ├── tsconfig.node.json ├── package.json ├── tsconfig.app.json ├── vite.config.ts └── components.d.ts ├── internal ├── dashboard │ ├── public │ │ └── resource.go │ ├── global │ │ ├── global.go │ │ └── constant │ │ │ └── constant.go │ ├── config │ │ ├── server.go │ │ └── config.go │ ├── response │ │ └── Result.go │ ├── middleware.go │ ├── server │ │ └── server.go │ └── handler │ │ ├── api.go │ │ └── config_api.go ├── agent │ ├── global │ │ └── global.go │ ├── report.go │ ├── config │ │ └── config.go │ ├── network_stats.go │ ├── mempool.go │ ├── adaptive.go │ ├── monitor.go │ └── gopsutil.go └── shared │ ├── app │ ├── logger.go │ ├── config.go │ └── app.go │ ├── logging │ └── logger.go │ ├── config │ └── loader.go │ └── errors │ ├── types.go │ └── handler.go ├── configs ├── sss-agent.yaml.example └── sss-dashboard.yaml.example ├── deployments ├── systemd │ └── sssa.service ├── caddy │ └── Caddyfile └── docker │ └── docker-compose.yml ├── .gitignore ├── .gitattributes ├── LICENSE ├── AGENTS.md ├── .dockerignore ├── scripts ├── build-dashboard.sh ├── build-web.sh ├── build-web.ps1 ├── build-docker.sh └── build-docker.ps1 ├── go.mod ├── pkg └── model │ ├── ServerStatusInfo.go │ └── RespServerInfo.go ├── cmd ├── agent │ └── main.go └── dashboard │ └── main.go ├── Dockerfile ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── .golangci.yml ├── Makefile ├── .goreleaser.yml └── docs ├── development ├── docker-build.md └── contributing.md └── architecture └── overview.md /web/.env.production: -------------------------------------------------------------------------------- 1 | VITE_MODE_NAME=production 2 | VITE_BASE_URL="/api" 3 | -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /web/.env.development: -------------------------------------------------------------------------------- 1 | 2 | # //开发.env.development 3 | VITE_MODE_NAME=development 4 | VITE_BASE_URL="/api" 5 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruanun/simple-server-status/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /internal/dashboard/public/resource.go: -------------------------------------------------------------------------------- 1 | package public 2 | 3 | import "embed" 4 | 5 | //go:embed dist 6 | var Resource embed.FS 7 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /internal/agent/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | // 构建信息变量(由 -ldflags 在编译时注入) 4 | var ( 5 | BuiltAt string 6 | GitCommit string 7 | Version string = "dev" 8 | GoVersion string 9 | ) 10 | -------------------------------------------------------------------------------- /internal/dashboard/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | // 构建信息变量(由 -ldflags 在编译时注入) 4 | var ( 5 | BuiltAt string 6 | GitCommit string 7 | Version string = "dev" 8 | GoVersion string 9 | ) 10 | -------------------------------------------------------------------------------- /web/src/locales/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n 类型定义 3 | * 定义翻译文件的结构,确保类型安全 4 | * @author ruan 5 | */ 6 | 7 | export type LocaleType = 'zh-CN' | 'en-US' 8 | 9 | export type LocaleMessages = Record 10 | -------------------------------------------------------------------------------- /web/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pinia Store 入口文件 3 | * @author ruan 4 | */ 5 | 6 | import { createPinia } from 'pinia' 7 | 8 | /** 9 | * 创建 Pinia 实例 10 | */ 11 | export const pinia = createPinia() 12 | -------------------------------------------------------------------------------- /configs/sss-agent.yaml.example: -------------------------------------------------------------------------------- 1 | serverAddr: ws://127.0.0.1:8900/ws-report 2 | serverId: x12ed #对应面板的配置 3 | authSecret: 1231331 #对应面板的配置 4 | 5 | disableIP2Region: false #非必填,禁用根据IP查询服务器区域信息,默认false 6 | logLevel: info #非必填,日志级别 默认info 7 | -------------------------------------------------------------------------------- /internal/dashboard/global/constant/constant.go: -------------------------------------------------------------------------------- 1 | package constant 2 | 3 | // HeaderSecret 定义认证密钥的 HTTP 头名称(这是头名称,不是实际凭证) 4 | // 5 | //nolint:gosec // G101: 这是 HTTP 头名称常量,不是硬编码的凭证值 6 | const HeaderSecret = "X-AUTH-SECRET" 7 | 8 | // HeaderId 定义服务器ID的 HTTP 头名称 9 | const HeaderId = "X-SERVER-ID" 10 | -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface ImportMetaEnv { 3 | readonly VITE_APP_TITLE: string 4 | readonly VITE_MODE_NAME: string 5 | readonly VITE_BASE_URL: string 6 | // 更多环境变量... 7 | } 8 | 9 | interface ImportMeta { 10 | readonly env: ImportMetaEnv 11 | } 12 | -------------------------------------------------------------------------------- /deployments/systemd/sssa.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Simple Server Status Agent 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | WorkingDirectory=/etc/sssa/ 8 | ExecStart=/etc/sssa/sss-agent -c /etc/sssa/sss-agent.yaml 9 | Restart=on-failure 10 | RestartSec=5s 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /web/src/utils/CommonUtil.ts: -------------------------------------------------------------------------------- 1 | 2 | export function readableBytes(bytes: number) { 3 | if (!bytes) { 4 | return '0B' 5 | } 6 | var i = Math.floor(Math.log(bytes) / Math.log(1024)), 7 | sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; 8 | return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + sizes[i]; 9 | } -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import 'ant-design-vue/dist/reset.css' 4 | import i18n from './locales' 5 | import { pinia } from './stores' 6 | 7 | const app = createApp(App) 8 | 9 | // 集成 Pinia 状态管理 10 | app.use(pinia) 11 | 12 | // 集成 i18n 国际化 13 | app.use(i18n) 14 | 15 | app.mount('#app') 16 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | node_modules 4 | dist 5 | !dashboard/public/dist/README.md 6 | *.log 7 | *.iml 8 | sss-agent.yaml 9 | sss-dashboard.yaml 10 | logs 11 | *.exe 12 | 13 | # 构建产物 14 | bin/ 15 | *.test 16 | 17 | # 测试覆盖率 18 | coverage.out 19 | coverage.html 20 | *.coverprofile 21 | 22 | # 临时文件 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # OS 特定 28 | .DS_Store 29 | Thumbs.db -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SimpleServerStatus 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Git 属性配置文件 2 | # 规范跨平台文件编码和行尾处理 3 | 4 | # PowerShell 脚本:UTF-8 编码 + CRLF 行尾 5 | *.ps1 text eol=crlf working-tree-encoding=UTF-8 6 | 7 | # Shell 脚本:UTF-8 编码 + LF 行尾 8 | *.sh text eol=lf 9 | 10 | # Go 源代码:UTF-8 编码 + LF 行尾 11 | *.go text eol=lf 12 | 13 | # YAML 配置文件:UTF-8 编码 + LF 行尾 14 | *.yml text eol=lf 15 | *.yaml text eol=lf 16 | 17 | # Markdown 文档:UTF-8 编码 + LF 行尾 18 | *.md text eol=lf 19 | 20 | # JSON 文件:UTF-8 编码 + LF 行尾 21 | *.json text eol=lf 22 | 23 | # 二进制文件:不进行任何转换 24 | *.exe binary 25 | *.dll binary 26 | *.so binary 27 | *.dylib binary 28 | *.png binary 29 | *.jpg binary 30 | *.jpeg binary 31 | *.gif binary 32 | *.ico binary 33 | *.pdf binary 34 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sssd-web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vue-tsc -b && vite build", 9 | "build:prod": "vite build --mode production", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons-vue": "^7.0.1", 14 | "ant-design-vue": "^4.2.6", 15 | "axios": "^1.7.9", 16 | "dayjs": "^1.11.13", 17 | "flag-icons-svg": "^0.0.3", 18 | "pinia": "^3.0.4", 19 | "vue": "^3.5.13", 20 | "vue-i18n": "^9.14.5" 21 | }, 22 | "devDependencies": { 23 | "@intlify/unplugin-vue-i18n": "^11.0.1", 24 | "@types/node": "^22.10.1", 25 | "@vitejs/plugin-vue": "^5.2.1", 26 | "typescript": "~5.6.2", 27 | "unplugin-vue-components": "^0.27.5", 28 | "vite": "^6.0.1", 29 | "vue-tsc": "^2.1.10" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "preserve", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | }, 28 | "allowSyntheticDefaultImports": true 29 | }, 30 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 31 | } 32 | -------------------------------------------------------------------------------- /internal/agent/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type AgentConfig struct { 4 | //服务器地址 5 | ServerAddr string `yaml:"serverAddr" validate:"required"` 6 | //每台机子对应id;唯一;在服务端配置 7 | ServerId string `yaml:"serverId" validate:"required"` 8 | //对应服务器配置的;做授权 9 | AuthSecret string `yaml:"authSecret" validate:"required"` 10 | //上报间隔,单位秒;默认2秒,最小值2 11 | ReportTimeInterval int `yaml:"reportTimeInterval"` 12 | //禁用根据IP查询服务器区域信息,默认false 13 | DisableIP2Region bool `yaml:"disableIP2Region"` 14 | 15 | //日志配置,日志级别 16 | LogPath string `yaml:"logPath"` 17 | //日志级别 debug,info,warn 默认info 18 | LogLevel string `yaml:"logLevel"` 19 | } 20 | 21 | // Validate 实现 ConfigLoader 接口 - 验证配置 22 | func (c *AgentConfig) Validate() error { 23 | // 基础验证会在配置加载时自动完成 24 | // 详细验证在 main 函数中通过 ValidateAndSetDefaults 完成 25 | return nil 26 | } 27 | 28 | // OnReload 实现 ConfigLoader 接口 - 配置重载时的回调 29 | func (c *AgentConfig) OnReload() error { 30 | // 配置重载后的处理在 app.LoadConfig 的回调中完成 31 | // 这里留空以避免循环导入 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ruan 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. 22 | -------------------------------------------------------------------------------- /internal/shared/app/logger.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ruanun/simple-server-status/internal/shared/logging" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | // LoggerConfig 日志配置 11 | type LoggerConfig struct { 12 | Level string 13 | FilePath string 14 | MaxSize int 15 | MaxAge int 16 | Compress bool 17 | LocalTime bool 18 | } 19 | 20 | // DefaultLoggerConfig 返回默认日志配置 21 | func DefaultLoggerConfig() LoggerConfig { 22 | return LoggerConfig{ 23 | MaxSize: 64, 24 | MaxAge: 5, 25 | Compress: true, 26 | LocalTime: true, 27 | } 28 | } 29 | 30 | // InitLogger 初始化日志器 31 | func InitLogger(level, filePath string) (*zap.SugaredLogger, error) { 32 | cfg := DefaultLoggerConfig() 33 | cfg.Level = level 34 | cfg.FilePath = filePath 35 | 36 | logger, err := logging.New(logging.Config{ 37 | Level: cfg.Level, 38 | FilePath: cfg.FilePath, 39 | MaxSize: cfg.MaxSize, 40 | MaxAge: cfg.MaxAge, 41 | Compress: cfg.Compress, 42 | LocalTime: cfg.LocalTime, 43 | }) 44 | 45 | if err != nil { 46 | return nil, fmt.Errorf("初始化日志失败: %w", err) 47 | } 48 | 49 | return logger, nil 50 | } 51 | -------------------------------------------------------------------------------- /web/src/services/serverService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务器数据服务层 3 | * 4 | * 职责: 5 | * - 封装服务器数据的 HTTP API 调用 6 | * - 提供统一的数据获取接口 7 | * - 处理 API 错误 8 | * 9 | * 使用方式: 10 | * ```typescript 11 | * import { serverService } from '@/services/serverService' 12 | * 13 | * try { 14 | * const data = await serverService.fetchServerInfo() 15 | * // 处理数据 16 | * } catch (error) { 17 | * // 处理错误 18 | * } 19 | * ``` 20 | * 21 | * @author ruan 22 | */ 23 | 24 | import http from '@/api' 25 | import type { ServerInfo } from '@/api/models' 26 | 27 | /** 28 | * 服务器数据服务类 29 | */ 30 | export class ServerService { 31 | /** 32 | * 获取所有服务器状态信息 33 | * @returns Promise 服务器信息数组 34 | * @throws Error 当 HTTP 请求失败时抛出 35 | */ 36 | async fetchServerInfo(): Promise { 37 | try { 38 | const response = await http.get>("/server/statusInfo") 39 | return response.data 40 | } catch (error) { 41 | console.error('Failed to fetch server info:', error) 42 | throw error 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * 服务器数据服务单例实例 49 | */ 50 | export const serverService = new ServerService() 51 | -------------------------------------------------------------------------------- /web/src/api/models/index.ts: -------------------------------------------------------------------------------- 1 | export interface ServerInfo { 2 | name: string; 3 | group: string; 4 | id: string; 5 | lastReportTime: number; 6 | uptime: number; 7 | platform: string; 8 | 9 | cpuPercent: number; 10 | RAMPercent: number; 11 | SWAPPercent: number; 12 | diskPercent: number; 13 | netInSpeed: number; 14 | netOutSpeed: number; 15 | 16 | isOnline: boolean 17 | 18 | hostInfo: HostInfo; 19 | 20 | loc: string; 21 | } 22 | 23 | export interface HostInfo { 24 | cpuInfo: string[]; 25 | avgStat: AvgStat; 26 | RAMTotal: number; 27 | RAMUsed: number; 28 | swapTotal: number; 29 | swapUsed: number; 30 | diskTotal: number; 31 | diskUsed: number; 32 | diskPartitions: DiskPartition[]; 33 | netInTransfer: number; 34 | netOutTransfer: number; 35 | } 36 | 37 | export interface DiskPartition { 38 | mountPoint: string; 39 | fstype: string; 40 | total: number; 41 | free: number; 42 | used: number; 43 | usedPercent: number; 44 | } 45 | 46 | export interface AvgStat { 47 | load1: number; 48 | load5: number; 49 | load15: number; 50 | } -------------------------------------------------------------------------------- /web/src/composables/useErrorHandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 统一错误处理 Composable 3 | * 4 | * 职责: 5 | * - 提供统一的 HTTP 错误处理 6 | * - 提供统一的 WebSocket 错误处理 7 | * - 统一的错误日志和用户提示 8 | * 9 | * 使用方式: 10 | * ```typescript 11 | * import { useErrorHandler } from '@/composables/useErrorHandler' 12 | * const { handleHttpError, handleWebSocketError } = useErrorHandler() 13 | * 14 | * try { 15 | * await someHttpRequest() 16 | * } catch (error) { 17 | * handleHttpError(error, '获取数据') 18 | * } 19 | * ``` 20 | * 21 | * @author ruan 22 | */ 23 | 24 | import { message } from 'ant-design-vue' 25 | 26 | export function useErrorHandler() { 27 | /** 28 | * 处理 HTTP 请求错误 29 | * @param error 错误对象 30 | * @param context 操作上下文(用于错误消息) 31 | */ 32 | function handleHttpError(error: any, context: string) { 33 | console.error(`${context}失败:`, error) 34 | message.error(`${context}失败,请稍后重试`) 35 | } 36 | 37 | /** 38 | * 处理 WebSocket 连接错误 39 | * @param error 错误对象 40 | */ 41 | function handleWebSocketError(error: any) { 42 | console.error('WebSocket 错误:', error) 43 | message.warning('WebSocket 连接失败,已切换到 HTTP 轮询模式') 44 | } 45 | 46 | return { 47 | handleHttpError, 48 | handleWebSocketError 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/dashboard/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type DashboardConfig struct { 4 | Address string `yaml:"address" json:"address"` //监听的地址;默认0.0.0.0 5 | Debug bool `yaml:"debug" json:"debug"` 6 | Port int `yaml:"port" json:"port"` //监听的端口; 默认8900 7 | WebSocketPath string `yaml:"webSocketPath" json:"webSocketPath"` //agent WebSocket路径 默认ws-report 8 | ReportTimeIntervalMax int `yaml:"reportTimeIntervalMax" json:"reportTimeIntervalMax"` //上报最大间隔;单位:秒 最小值5 默认值:30;离线判定,超过这个值既视为离线 9 | Servers []*ServerConfig `yaml:"servers" validate:"required,dive,required" json:"servers"` 10 | 11 | //日志配置,日志级别 12 | LogPath string `yaml:"logPath"` 13 | LogLevel string `yaml:"logLevel"` 14 | } 15 | 16 | // Validate 实现 ConfigLoader 接口 - 验证配置 17 | func (c *DashboardConfig) Validate() error { 18 | // 基础验证会在配置加载时自动完成 19 | // 详细验证在 main 函数中通过 ValidateAndApplyDefaults 完成 20 | return nil 21 | } 22 | 23 | // OnReload 实现 ConfigLoader 接口 - 配置重载时的回调 24 | func (c *DashboardConfig) OnReload() error { 25 | // 配置重载后的处理在 app.LoadConfig 的回调中完成 26 | // 这里留空以避免循环导入 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /web/src/api/helper/checkStatus.ts: -------------------------------------------------------------------------------- 1 | import {message} from "ant-design-vue"; 2 | 3 | /** 4 | * @description: 校验网络请求状态码 5 | * @param {Number} status 6 | * @return void 7 | */ 8 | export const checkStatus = (status: number): void => { 9 | switch (status) { 10 | case 400: 11 | message.error("请求失败!请您稍后重试"); 12 | break; 13 | case 401: 14 | message.error("登录失效!请您重新登录"); 15 | break; 16 | case 403: 17 | message.error("当前账号无权限访问!"); 18 | break; 19 | case 404: 20 | message.error("你所访问的资源不存在!"); 21 | break; 22 | case 405: 23 | message.error("请求方式错误!请您稍后重试"); 24 | break; 25 | case 408: 26 | message.error("请求超时!请您稍后重试"); 27 | break; 28 | case 500: 29 | message.error("服务异常!"); 30 | break; 31 | case 502: 32 | message.error("网关错误!"); 33 | break; 34 | case 503: 35 | message.error("服务不可用!"); 36 | break; 37 | case 504: 38 | message.error("网关超时!"); 39 | break; 40 | default: 41 | message.error("请求失败!"); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /web/src/constants/connectionModes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 连接模式常量定义 3 | * 4 | * 职责: 5 | * - 定义连接模式的标准常量 6 | * - 提供连接模式的显示名称 7 | * - 统一连接模式的类型定义 8 | * 9 | * @author ruan 10 | */ 11 | 12 | /** 13 | * 连接模式常量 14 | * 使用 as const 确保类型安全 15 | */ 16 | export const CONNECTION_MODES = { 17 | /** WebSocket 实时模式 */ 18 | WEBSOCKET: 'websocket', 19 | /** HTTP 轮询模式 */ 20 | HTTP: 'http' 21 | } as const 22 | 23 | /** 24 | * 连接模式类型 25 | * 从 CONNECTION_MODES 推导出联合类型 26 | */ 27 | export type ConnectionMode = typeof CONNECTION_MODES[keyof typeof CONNECTION_MODES] 28 | 29 | /** 30 | * 连接模式显示名称(中文) 31 | */ 32 | export const CONNECTION_MODE_LABELS_ZH = { 33 | [CONNECTION_MODES.WEBSOCKET]: 'WebSocket 实时模式', 34 | [CONNECTION_MODES.HTTP]: 'HTTP 轮询模式' 35 | } as const 36 | 37 | /** 38 | * 连接模式显示名称(英文) 39 | */ 40 | export const CONNECTION_MODE_LABELS_EN = { 41 | [CONNECTION_MODES.WEBSOCKET]: 'WebSocket Real-time', 42 | [CONNECTION_MODES.HTTP]: 'HTTP Polling' 43 | } as const 44 | 45 | /** 46 | * 根据语言获取连接模式的显示名称 47 | * @param mode 连接模式 48 | * @param locale 语言环境,默认为中文 49 | * @returns 显示名称 50 | */ 51 | export function getConnectionModeLabel(mode: ConnectionMode, locale: string = 'zh-CN'): string { 52 | const labels = locale === 'zh-CN' ? CONNECTION_MODE_LABELS_ZH : CONNECTION_MODE_LABELS_EN 53 | return labels[mode] 54 | } 55 | -------------------------------------------------------------------------------- /web/src/types/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP API 类型定义 3 | * 4 | * 职责: 5 | * - 定义 HTTP 响应的标准格式 6 | * - 提供响应码枚举 7 | * - 统一错误类型定义 8 | * 9 | * @author ruan 10 | */ 11 | 12 | /** 13 | * API 响应码枚举 14 | * 标准的 HTTP 状态码 15 | */ 16 | export enum ResponseCode { 17 | /** 成功 */ 18 | SUCCESS = 200, 19 | /** 客户端错误 */ 20 | BAD_REQUEST = 400, 21 | /** 未授权 */ 22 | UNAUTHORIZED = 401, 23 | /** 未找到 */ 24 | NOT_FOUND = 404, 25 | /** 服务器错误 */ 26 | INTERNAL_ERROR = 500, 27 | /** 请求超时 */ 28 | TIMEOUT = 30000 29 | } 30 | 31 | /** 32 | * API 响应基础接口 33 | * 不包含 data 字段 34 | */ 35 | export interface ApiResult { 36 | code: ResponseCode 37 | message: string 38 | } 39 | 40 | /** 41 | * API 响应接口(包含 data) 42 | * 所有成功的 API 响应都应该包含 data 字段 43 | */ 44 | export interface ApiResponse extends ApiResult { 45 | data?: T 46 | } 47 | 48 | /** 49 | * API 错误接口 50 | * 统一的错误格式 51 | */ 52 | export interface ApiError { 53 | code: ResponseCode 54 | message: string 55 | originalError?: any 56 | } 57 | 58 | /** 59 | * 类型守卫:判断响应是否成功 60 | */ 61 | export function isSuccessResponse(response: ApiResponse): boolean { 62 | return response.code === ResponseCode.SUCCESS 63 | } 64 | 65 | /** 66 | * 类型守卫:判断是否为 ApiError 67 | */ 68 | export function isApiError(error: any): error is ApiError { 69 | return error && typeof error.code === 'number' && typeof error.message === 'string' 70 | } 71 | -------------------------------------------------------------------------------- /web/src/composables/useServerInfoFormatting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务器信息格式化 Composable 3 | * 4 | * 职责: 5 | * - 统一管理服务器信息相关的格式化函数导入 6 | * - 提供一致的格式化函数访问接口 7 | * - 减少重复的导入语句 8 | * 9 | * 使用方式: 10 | * ```typescript 11 | * import { useServerInfoFormatting } from '@/composables/useServerInfoFormatting' 12 | * 13 | * const { 14 | * readableBytes, 15 | * formatUptime, 16 | * formatPercent, 17 | * formatLoad, 18 | * getPercentColor 19 | * } = useServerInfoFormatting() 20 | * ``` 21 | * 22 | * @author ruan 23 | */ 24 | 25 | import { readableBytes } from '@/utils/CommonUtil' 26 | import { formatUptime, formatPercent, formatLoad } from '@/utils/formatters' 27 | import { getPercentColor } from '@/utils/colorUtils' 28 | 29 | /** 30 | * 服务器信息格式化工具集合 31 | * 统一导出所有格式化相关函数 32 | */ 33 | export function useServerInfoFormatting() { 34 | return { 35 | /** 36 | * 字节数格式化为易读字符串 37 | * @example readableBytes(1024) // "1.0 KB" 38 | */ 39 | readableBytes, 40 | 41 | /** 42 | * 运行时间格式化(秒 → 天/小时/分钟) 43 | * @example formatUptime(86400) // "1天 0小时" 44 | */ 45 | formatUptime, 46 | 47 | /** 48 | * 百分比格式化(保留整数) 49 | * @example formatPercent(75.6) // 76 50 | */ 51 | formatPercent, 52 | 53 | /** 54 | * 系统负载格式化 55 | * @example formatLoad(1.2, 1.5, 1.9) // "1.2 / 1.5 / 1.9" 56 | */ 57 | formatLoad, 58 | 59 | /** 60 | * 根据百分比返回对应颜色 61 | * @example getPercentColor(95) // "#ff4d4f" (红色) 62 | */ 63 | getPercentColor 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /internal/dashboard/middleware.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // CORSMiddleware CORS中间件 10 | func CORSMiddleware() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | // 获取请求的 Origin 13 | origin := c.Request.Header.Get("Origin") 14 | if origin != "" { 15 | // 当有 Origin 时,回显实际的 Origin 而不是使用通配符 16 | // 这样可以支持 credentials 而不违反 CORS 规范 17 | c.Header("Access-Control-Allow-Origin", origin) 18 | c.Header("Access-Control-Allow-Credentials", "true") 19 | // 添加 Vary: Origin 头防止中间缓存导致的 CORS 错误 20 | // 确保不同 origin 的请求不会复用相同的缓存响应 21 | c.Header("Vary", "Origin") 22 | } 23 | 24 | c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") 25 | c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 26 | c.Header("Access-Control-Expose-Headers", "Content-Length, X-Response-Time, X-Cache") 27 | 28 | if c.Request.Method == "OPTIONS" { 29 | c.AbortWithStatus(http.StatusNoContent) 30 | return 31 | } 32 | 33 | c.Next() 34 | } 35 | } 36 | 37 | // SecurityMiddleware 安全中间件 38 | func SecurityMiddleware() gin.HandlerFunc { 39 | return func(c *gin.Context) { 40 | // 安全头 41 | c.Header("X-Content-Type-Options", "nosniff") 42 | c.Header("X-Frame-Options", "DENY") 43 | c.Header("X-XSS-Protection", "1; mode=block") 44 | c.Header("Referrer-Policy", "strict-origin-when-cross-origin") 45 | c.Header("Content-Security-Policy", "default-src 'self'") 46 | 47 | c.Next() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # Repository Guidelines 2 | 3 | ## Project Structure & Module Organization 4 | - `cmd/agent`:Agent 入口;`cmd/dashboard`:Dashboard 入口。 5 | - `internal/agent`、`internal/dashboard`:核心业务与适配层。 6 | - `pkg/model`:共享数据模型。 7 | - `web/`:前端(Vue 3 + TypeScript + Vite)。 8 | - `configs/*.yaml.example`:配置模板(复制为根目录同名文件使用)。 9 | - `scripts/`、`deployments/`、`docs/`:构建脚本、部署清单与技术文档。 10 | 11 | ## Build, Test, and Development Commands 12 | - `make build`:构建 Agent 与 Dashboard 二进制产物。 13 | - `make build-agent` / `make build-dashboard`:分别构建两端;Dashboard 构建会打包前端。 14 | - `make dev-web`:启动前端开发(等同 `cd web && pnpm run dev`)。 15 | - `make run-agent` / `make run-dashboard`:本地运行二进制。 16 | - `make test` / `make test-coverage` / `make race`:测试、覆盖率与竞态检测。 17 | - `make lint` / `make check` / `make tidy`:静态检查、综合检查与依赖整理。 18 | 19 | ## Coding Style & Naming Conventions 20 | - Go:使用 `gofmt`/`goimports` 保持格式;`golangci-lint` 与 `gosec` 做静态与安全检查。 21 | - 包/文件名小写且语义清晰;错误显式处理;使用 `zap` 记录结构化日志。 22 | - 前端:2 空格缩进;组件 `PascalCase.vue`,模块 `kebab-case`;TypeScript 优先、类型完备。 23 | 24 | ## Testing Guidelines 25 | - Go 标准测试:文件命名 `*_test.go`,优先表驱动测试;覆盖关键路径。 26 | - 生成覆盖率:`make test-coverage`(输出 `coverage.html`)。 27 | - 前端当前未配置测试框架,新增建议采用 Vitest。 28 | 29 | ## Commit & Pull Request Guidelines 30 | - 建议遵循 Conventional Commits:如 `feat(agent): add NIC stats`、`fix(dashboard): ws reconnect`。 31 | - PR 必须:清晰描述、关联 Issue、包含测试说明;UI 变更附截图;涉及公共接口/行为需更新文档与示例配置。 32 | 33 | ## Security & Configuration Tips 34 | - 勿提交真实密钥/证书。复制示例为本地配置: 35 | - Linux/macOS:`cp configs/sss-agent.yaml.example sss-agent.yaml` 36 | - Windows:`Copy-Item configs\sss-agent.yaml.example sss-agent.yaml` 37 | - 生产部署按需调整端口、日志与鉴权;以最小权限运行二进制。 38 | 39 | 40 | -------------------------------------------------------------------------------- /web/src/locales/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n 国际化配置 3 | * 支持中英文切换,根据浏览器语言自动检测 4 | * @author ruan 5 | */ 6 | 7 | import { createI18n } from 'vue-i18n' 8 | import type { LocaleType } from './types' 9 | import zhCN from './zh-CN' 10 | import enUS from './en-US' 11 | 12 | const LOCALE_STORAGE_KEY = 'app-locale' 13 | 14 | /** 15 | * 检测浏览器语言 16 | * 中文优先:zh/zh-CN/zh-TW → zh-CN 17 | * 其他语言 → en-US 18 | */ 19 | function detectBrowserLanguage(): LocaleType { 20 | // 先检查 localStorage 中是否有保存的语言设置 21 | const savedLocale = localStorage.getItem(LOCALE_STORAGE_KEY) 22 | if (savedLocale === 'zh-CN' || savedLocale === 'en-US') { 23 | return savedLocale as LocaleType 24 | } 25 | 26 | // 获取浏览器语言 27 | const browserLang = navigator.language.toLowerCase() 28 | 29 | // 中文语言检测(zh, zh-cn, zh-tw, zh-hk 等) 30 | if (browserLang.startsWith('zh')) { 31 | return 'zh-CN' 32 | } 33 | 34 | // 默认使用英文 35 | return 'en-US' 36 | } 37 | 38 | /** 39 | * 创建 i18n 实例 40 | */ 41 | const i18n = createI18n({ 42 | legacy: false, // 使用 Composition API 模式 43 | locale: detectBrowserLanguage(), 44 | fallbackLocale: 'en-US', 45 | messages: { 46 | 'zh-CN': zhCN, 47 | 'en-US': enUS 48 | }, 49 | globalInjection: true // 全局注入 $t 函数 50 | }) 51 | 52 | /** 53 | * 切换语言 54 | * @param locale 目标语言 55 | */ 56 | export function setLocale(locale: LocaleType) { 57 | i18n.global.locale.value = locale 58 | localStorage.setItem(LOCALE_STORAGE_KEY, locale) 59 | 60 | // 更新 HTML lang 属性 61 | document.documentElement.lang = locale 62 | } 63 | 64 | /** 65 | * 获取当前语言 66 | */ 67 | export function getLocale(): LocaleType { 68 | return i18n.global.locale.value as LocaleType 69 | } 70 | 71 | export default i18n 72 | -------------------------------------------------------------------------------- /internal/dashboard/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-contrib/static" 8 | ginzap "github.com/gin-contrib/zap" 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | "go.uber.org/zap/zapcore" 12 | 13 | internal "github.com/ruanun/simple-server-status/internal/dashboard" 14 | "github.com/ruanun/simple-server-status/internal/dashboard/config" 15 | "github.com/ruanun/simple-server-status/internal/dashboard/public" 16 | ) 17 | 18 | func InitServer(cfg *config.DashboardConfig, logger *zap.SugaredLogger, errorHandler *internal.ErrorHandler) *gin.Engine { 19 | if !cfg.Debug { 20 | gin.SetMode(gin.ReleaseMode) 21 | } 22 | r := gin.New() 23 | 24 | // 安全中间件 25 | r.Use(internal.SecurityMiddleware()) 26 | 27 | // CORS中间件 28 | r.Use(internal.CORSMiddleware()) 29 | 30 | // 使用自定义的错误处理中间件 31 | r.Use(internal.PanicRecoveryMiddleware(errorHandler)) 32 | r.Use(internal.ErrorMiddleware(errorHandler)) 33 | 34 | //gin使用zap日志 35 | r.Use(ginzap.GinzapWithConfig(logger.Desugar(), &ginzap.Config{TimeFormat: "2006-01-02 15:04:05.000", UTC: true, DefaultLevel: zapcore.DebugLevel})) 36 | 37 | //静态网页 38 | staticServer := static.Serve("/", static.EmbedFolder(public.Resource, "dist")) 39 | r.Use(staticServer) 40 | 41 | r.NoRoute(func(c *gin.Context) { 42 | //是get请求,路径不是以api开头的跳转到首页 43 | if c.Request.Method == http.MethodGet && 44 | !strings.ContainsRune(c.Request.URL.Path, '.') && 45 | !strings.HasPrefix(c.Request.URL.Path, "/api/") { 46 | 47 | //这里直接响应到首页非跳转;转发 48 | //c.Request.URL.Path = "/" 49 | //staticServer(c) 50 | 51 | //这里301跳转 52 | c.Redirect(http.StatusMovedPermanently, "/") 53 | } 54 | }) 55 | 56 | return r 57 | } 58 | -------------------------------------------------------------------------------- /deployments/caddy/Caddyfile: -------------------------------------------------------------------------------- 1 | # Simple Server Status Caddyfile 2 | # Caddy 自动处理 HTTPS 证书 3 | 4 | # 主站点配置 5 | # 将 localhost 替换为你的域名,如 example.com 6 | localhost { 7 | # 反向代理到 Dashboard 8 | reverse_proxy dashboard:8900 9 | 10 | # 启用压缩 11 | encode gzip 12 | 13 | # 安全头 14 | header { 15 | # 安全相关头部 16 | Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 17 | X-Content-Type-Options "nosniff" 18 | X-Frame-Options "DENY" 19 | X-XSS-Protection "1; mode=block" 20 | Referrer-Policy "strict-origin-when-cross-origin" 21 | 22 | # 移除服务器信息 23 | -Server 24 | } 25 | 26 | # 静态资源缓存 27 | @static { 28 | path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot 29 | } 30 | header @static { 31 | Cache-Control "public, max-age=31536000, immutable" 32 | } 33 | 34 | # WebSocket 支持(Caddy 自动处理 WebSocket 升级) 35 | # 无需特殊配置,Caddy 会自动检测和处理 WebSocket 连接 36 | 37 | # 日志配置 38 | log { 39 | output file /var/log/caddy/access.log { 40 | roll_size 10MB 41 | roll_keep 5 42 | } 43 | format json 44 | } 45 | 46 | # 健康检查端点 47 | handle /health { 48 | respond "healthy" 200 49 | } 50 | } 51 | 52 | # 如果需要多个域名,可以添加更多配置块 53 | # example.com { 54 | # reverse_proxy dashboard:8900 55 | # encode gzip 56 | # } 57 | 58 | # 全局选项 59 | { 60 | # 自动 HTTPS 61 | auto_https on 62 | 63 | # 邮箱用于 Let's Encrypt(生产环境请修改) 64 | email admin@example.com 65 | 66 | # 管理端点(可选,用于监控) 67 | admin localhost:2019 68 | 69 | # 日志级别 70 | log { 71 | level INFO 72 | } 73 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # ============================================ 2 | # Docker 构建忽略文件 3 | # 作用: 减少构建上下文大小,加快构建速度 4 | # ============================================ 5 | 6 | # Git 相关 7 | .git 8 | .gitignore 9 | .gitattributes 10 | .github 11 | 12 | # 文档 13 | *.md 14 | docs/ 15 | INSTALL.md 16 | LICENSE 17 | CHANGELOG.md 18 | 19 | # IDE 和编辑器 20 | .vscode/ 21 | .idea/ 22 | *.swp 23 | *.swo 24 | *~ 25 | .DS_Store 26 | 27 | # 构建产物 28 | bin/ 29 | dist/ 30 | build/ 31 | *.exe 32 | *.dll 33 | *.so 34 | *.dylib 35 | sss-agent 36 | sss-dashboard 37 | sssd 38 | 39 | # Go 相关 40 | vendor/ 41 | *.test 42 | *.out 43 | coverage.txt 44 | *.prof 45 | 46 | # Node.js 相关(但保留 package.json 和 package-lock.json) 47 | web/node_modules/ 48 | web/dist/ 49 | web/.nuxt/ 50 | web/.output/ 51 | web/.vite/ 52 | web/.cache/ 53 | 54 | # 日志 55 | *.log 56 | .logs/ 57 | logs/ 58 | 59 | # 临时文件 60 | tmp/ 61 | temp/ 62 | *.tmp 63 | *.bak 64 | *.swp 65 | 66 | # 测试相关 67 | test/ 68 | tests/ 69 | *_test.go 70 | testdata/ 71 | 72 | # 部署相关(这些文件在镜像中不需要) 73 | deployments/ 74 | docker-compose.yml 75 | docker-compose.*.yml 76 | Dockerfile.* 77 | *.dockerfile 78 | 79 | # CI/CD 80 | .github/ 81 | .gitlab-ci.yml 82 | .travis.yml 83 | azure-pipelines.yml 84 | 85 | # 配置文件示例(不需要打包到镜像) 86 | configs/ 87 | *.yaml.example 88 | *.yml.example 89 | 90 | # GoReleaser 91 | .goreleaser.yml 92 | goreleaser.yml 93 | dist/ 94 | 95 | # 脚本(构建时不需要) 96 | scripts/ 97 | 98 | # 环境变量文件 99 | # 排除可能包含敏感信息的环境变量文件 100 | .env 101 | .env.local 102 | .env.development.local 103 | .env.test.local 104 | .env.production.local 105 | 106 | # 但保留前端生产构建所需的配置文件(仅包含非敏感的公开配置) 107 | !web/.env.production 108 | 109 | # 其他敏感文件 110 | *.pem 111 | *.key 112 | *.crt 113 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import {fileURLToPath, URL} from 'node:url' 4 | import Components from 'unplugin-vue-components/vite'; 5 | import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'; 6 | 7 | export default defineConfig({ 8 | build: { 9 | //分隔多个,防止单文件过大 10 | rollupOptions: { 11 | output:{ 12 | manualChunks(id) { 13 | if (id.includes('node_modules')) { 14 | return id.toString().split('node_modules/')[1].split('/')[0].toString(); 15 | } 16 | } 17 | } 18 | } 19 | }, 20 | plugins: [ 21 | vue(), 22 | Components({ 23 | resolvers: [ 24 | AntDesignVueResolver({ 25 | importStyle: false, // css in js 26 | }), 27 | ], 28 | }), 29 | ], 30 | resolve: { 31 | // alias: { 32 | // '@': path.resolve(__dirname, 'src'), 33 | // } 34 | alias: { 35 | '@': fileURLToPath(new URL('./src', import.meta.url)) 36 | } 37 | }, 38 | server: { 39 | proxy: { 40 | "/api": { 41 | target: "http://127.0.0.1:8900", 42 | changeOrigin: true, //允许跨域 43 | ws: true, // 开启 websockets 代理 44 | secure: false, // 验证 SSL 证书 45 | rewrite: (path) => path, 46 | }, 47 | "/ws-frontend": { 48 | target: "http://127.0.0.1:8900", 49 | changeOrigin: true, //允许跨域 50 | ws: true, // 开启 websockets 代理 51 | secure: false, // 验证 SSL 证书 52 | }, 53 | } 54 | }, 55 | }) 56 | -------------------------------------------------------------------------------- /web/src/types/websocket.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * WebSocket 消息类型定义 3 | * 4 | * 职责: 5 | * - 定义 WebSocket 消息的完整类型结构 6 | * - 提供类型安全的消息解析 7 | * - 统一消息格式规范 8 | * 9 | * @author ruan 10 | */ 11 | 12 | import type { ServerInfo } from '@/api/models' 13 | 14 | /** 15 | * WebSocket 消息基础接口 16 | * 所有 WebSocket 消息都应该包含 type 字段 17 | */ 18 | export interface WebSocketMessage { 19 | type: string 20 | data?: T 21 | } 22 | 23 | /** 24 | * 服务器状态更新消息 25 | * 从后端推送的服务器状态数据 26 | */ 27 | export interface ServerStatusUpdateMessage extends WebSocketMessage { 28 | type: 'server_status_update' 29 | data: ServerInfo[] 30 | } 31 | 32 | /** 33 | * Ping 消息(客户端发送) 34 | * 用于心跳保活 35 | */ 36 | export interface PingMessage extends WebSocketMessage { 37 | type: 'ping' 38 | } 39 | 40 | /** 41 | * Pong 消息(服务端响应) 42 | * 心跳响应 43 | */ 44 | export interface PongMessage extends WebSocketMessage { 45 | type: 'pong' 46 | } 47 | 48 | /** 49 | * 接收消息的联合类型 50 | * 包含所有可能从服务端接收到的消息类型 51 | */ 52 | export type IncomingMessage = ServerStatusUpdateMessage | PongMessage 53 | 54 | /** 55 | * 发送消息的联合类型 56 | * 包含所有可能发送到服务端的消息类型 57 | */ 58 | export type OutgoingMessage = PingMessage 59 | 60 | /** 61 | * 消息类型枚举 62 | * 用于类型检查和 switch 语句 63 | */ 64 | export const MessageType = { 65 | // 接收 66 | SERVER_STATUS_UPDATE: 'server_status_update', 67 | PONG: 'pong', 68 | // 发送 69 | PING: 'ping' 70 | } as const 71 | 72 | /** 73 | * 类型守卫:判断是否为服务器状态更新消息 74 | */ 75 | export function isServerStatusUpdateMessage( 76 | message: IncomingMessage 77 | ): message is ServerStatusUpdateMessage { 78 | return message.type === MessageType.SERVER_STATUS_UPDATE 79 | } 80 | 81 | /** 82 | * 类型守卫:判断是否为 Pong 消息 83 | */ 84 | export function isPongMessage(message: IncomingMessage): message is PongMessage { 85 | return message.type === MessageType.PONG 86 | } 87 | -------------------------------------------------------------------------------- /web/src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 简体中文翻译 3 | * @author ruan 4 | */ 5 | 6 | const zhCN = { 7 | serverInfo: { 8 | labels: { 9 | system: '系统', 10 | cpuUsage: 'CPU', 11 | memoryUsage: '内存', 12 | swapMemory: '交换区', 13 | networkSpeed: '网络', 14 | uptime: '运行时间', 15 | lastUpdate: '最后更新', 16 | cpuInfo: 'CPU', 17 | memoryDetails: '内存', 18 | systemLoad: '负载', 19 | totalTraffic: '流量', 20 | diskUsage: '磁盘' 21 | }, 22 | units: { 23 | percent: '%', 24 | mbps: 'MB/s', 25 | kbps: 'KB/s', 26 | bps: 'B/s', 27 | gb: 'GB', 28 | mb: 'MB', 29 | kb: 'KB', 30 | bytes: '字节', 31 | days: '天', 32 | hours: '小时', 33 | minutes: '分钟', 34 | seconds: '秒' 35 | }, 36 | table: { 37 | mountPoint: '挂载点', 38 | used: '已用', 39 | total: '总计' 40 | }, 41 | status: { 42 | online: '在线', 43 | offline: '离线' 44 | } 45 | }, 46 | common: { 47 | language: '语言', 48 | switchTo: '切换到', 49 | settings: '设置' 50 | }, 51 | header: { 52 | servers: '服务器', 53 | online: '在线', 54 | status: { 55 | connected: '已连接', 56 | connecting: '连接中', 57 | reconnecting: '重连中', 58 | disconnected: '已断开', 59 | error: '连接错误', 60 | unknown: '未知状态', 61 | httpPolling: 'HTTP 轮询' 62 | }, 63 | mode: { 64 | websocket: 'WebSocket', 65 | http: 'HTTP' 66 | }, 67 | actions: { 68 | switchToHttp: '切换到 HTTP 轮询模式', 69 | switchToWebsocket: '切换到 WebSocket 实时模式', 70 | reconnect: '重新连接 WebSocket' 71 | }, 72 | stats: { 73 | title: '连接统计', 74 | messages: '消息', 75 | reconnections: '重连', 76 | uptime: '连接时长', 77 | connectedSince: '连接于' 78 | } 79 | } 80 | } 81 | 82 | export default zhCN 83 | -------------------------------------------------------------------------------- /deployments/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Simple Server Status - Docker Compose 配置 2 | # 使用方法:将此文件复制到项目根目录,然后运行 docker-compose up -d 3 | # 注意:需要先准备配置文件和 Caddyfile(如果使用反向代理) 4 | 5 | version: '3.8' 6 | 7 | services: 8 | dashboard: 9 | # 方式 1:使用官方镜像(推荐) 10 | image: ruanun/sssd:latest 11 | 12 | # 方式 2:从源码构建(取消下方注释) 13 | # build: 14 | # context: ../.. # 项目根目录 15 | # dockerfile: Dockerfile 16 | 17 | container_name: sss-dashboard 18 | ports: 19 | - "8900:8900" 20 | volumes: 21 | # 配置文件:需要先创建 sss-dashboard.yaml 22 | # 示例:cp ../../configs/sss-dashboard.yaml.example ./sss-dashboard.yaml 23 | - ./sss-dashboard.yaml:/app/sss-dashboard.yaml:ro 24 | 25 | # 日志目录(可选) 26 | - dashboard-logs:/app/.logs 27 | environment: 28 | - CONFIG=/app/sss-dashboard.yaml 29 | - TZ=Asia/Shanghai 30 | restart: unless-stopped 31 | networks: 32 | - sss-network 33 | healthcheck: 34 | test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8900/api/statistics"] 35 | interval: 30s 36 | timeout: 10s 37 | retries: 3 38 | start_period: 40s 39 | 40 | # 可选:Caddy 反向代理(使用 HTTPS) 41 | # 启动命令:docker-compose --profile with-caddy up -d 42 | caddy: 43 | image: caddy:alpine 44 | container_name: sss-caddy 45 | ports: 46 | - "80:80" 47 | - "443:443" 48 | volumes: 49 | # Caddy 配置文件:需要先创建 Caddyfile 50 | # 示例:cp ../../deployments/caddy/Caddyfile ./Caddyfile 51 | - ./Caddyfile:/etc/caddy/Caddyfile:ro 52 | - caddy-data:/data 53 | - caddy-config:/config 54 | depends_on: 55 | - dashboard 56 | restart: unless-stopped 57 | networks: 58 | - sss-network 59 | profiles: 60 | - with-caddy 61 | 62 | networks: 63 | sss-network: 64 | driver: bridge 65 | 66 | volumes: 67 | dashboard-logs: 68 | driver: local 69 | caddy-data: 70 | driver: local 71 | caddy-config: 72 | driver: local 73 | -------------------------------------------------------------------------------- /web/src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * English translation 3 | * @author ruan 4 | */ 5 | 6 | const enUS = { 7 | serverInfo: { 8 | labels: { 9 | system: 'System', 10 | cpuUsage: 'CPU', 11 | memoryUsage: 'Memory', 12 | swapMemory: 'Swap', 13 | networkSpeed: 'Network', 14 | uptime: 'Uptime', 15 | lastUpdate: 'Updated', 16 | cpuInfo: 'CPU', 17 | memoryDetails: 'Memory', 18 | systemLoad: 'Load', 19 | totalTraffic: 'Traffic', 20 | diskUsage: 'Disk' 21 | }, 22 | units: { 23 | percent: '%', 24 | mbps: 'MB/s', 25 | kbps: 'KB/s', 26 | bps: 'B/s', 27 | gb: 'GB', 28 | mb: 'MB', 29 | kb: 'KB', 30 | bytes: 'Bytes', 31 | days: 'd', 32 | hours: 'h', 33 | minutes: 'm', 34 | seconds: 's' 35 | }, 36 | table: { 37 | mountPoint: 'Mount', 38 | used: 'Used', 39 | total: 'Total' 40 | }, 41 | status: { 42 | online: 'Online', 43 | offline: 'Offline' 44 | } 45 | }, 46 | common: { 47 | language: 'Language', 48 | switchTo: 'Switch to', 49 | settings: 'Settings' 50 | }, 51 | header: { 52 | servers: 'Servers', 53 | online: 'Online', 54 | status: { 55 | connected: 'Connected', 56 | connecting: 'Connecting', 57 | reconnecting: 'Reconnecting', 58 | disconnected: 'Disconnected', 59 | error: 'Connection Error', 60 | unknown: 'Unknown Status', 61 | httpPolling: 'HTTP Polling' 62 | }, 63 | mode: { 64 | websocket: 'WebSocket', 65 | http: 'HTTP' 66 | }, 67 | actions: { 68 | switchToHttp: 'Switch to HTTP Polling Mode', 69 | switchToWebsocket: 'Switch to WebSocket Real-time Mode', 70 | reconnect: 'Reconnect WebSocket' 71 | }, 72 | stats: { 73 | title: 'Connection Stats', 74 | messages: 'Messages', 75 | reconnections: 'Reconnections', 76 | uptime: 'Uptime', 77 | connectedSince: 'Connected since' 78 | } 79 | } 80 | } 81 | 82 | export default enUS 83 | -------------------------------------------------------------------------------- /internal/shared/app/config.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | sharedConfig "github.com/ruanun/simple-server-status/internal/shared/config" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | // ConfigLoader 配置加载器接口 11 | type ConfigLoader interface { 12 | // Validate 验证配置 13 | Validate() error 14 | // OnReload 配置重新加载时的回调 15 | OnReload() error 16 | } 17 | 18 | // LoadConfig 通用配置加载函数 19 | // onReloadCallback: 配置重载时的额外处理函数(可选) 20 | func LoadConfig[T ConfigLoader]( 21 | configName string, 22 | configType string, 23 | searchPaths []string, 24 | watchConfig bool, 25 | onReloadCallback func(T) error, 26 | ) (T, error) { 27 | var cfg T 28 | 29 | // 配置变更回调 30 | configChangeCallback := func(v *viper.Viper) error { 31 | var tempCfg T 32 | 33 | // 重新解析配置 34 | if err := v.Unmarshal(&tempCfg); err != nil { 35 | fmt.Printf("[ERROR] 重新解析配置失败: %v\n", err) 36 | return fmt.Errorf("配置反序列化失败: %w", err) 37 | } 38 | 39 | // 验证新配置 40 | if err := tempCfg.Validate(); err != nil { 41 | fmt.Printf("[ERROR] 配置验证失败: %v\n", err) 42 | return fmt.Errorf("配置验证失败: %w", err) 43 | } 44 | 45 | // 执行重载回调 46 | if err := tempCfg.OnReload(); err != nil { 47 | fmt.Printf("[ERROR] 配置重载失败: %v\n", err) 48 | return fmt.Errorf("配置重载失败: %w", err) 49 | } 50 | 51 | // 执行额外的回调处理 52 | if onReloadCallback != nil { 53 | if err := onReloadCallback(tempCfg); err != nil { 54 | fmt.Printf("[ERROR] 配置更新回调失败: %v\n", err) 55 | return fmt.Errorf("配置更新失败: %w", err) 56 | } 57 | } 58 | 59 | // 更新配置 60 | cfg = tempCfg 61 | fmt.Printf("[INFO] 配置已热加载并验证成功\n") 62 | 63 | return nil 64 | } 65 | 66 | // 加载配置 67 | _, err := sharedConfig.Load(sharedConfig.LoadOptions{ 68 | ConfigName: configName, 69 | ConfigType: configType, 70 | ConfigEnvKey: "CONFIG", 71 | SearchPaths: searchPaths, 72 | WatchConfigFile: watchConfig, 73 | OnConfigChange: configChangeCallback, 74 | }, &cfg) 75 | 76 | if err != nil { 77 | return cfg, fmt.Errorf("加载配置失败: %w", err) 78 | } 79 | 80 | // 验证初始配置 81 | if err := cfg.Validate(); err != nil { 82 | return cfg, fmt.Errorf("配置验证失败: %w", err) 83 | } 84 | 85 | return cfg, nil 86 | } 87 | -------------------------------------------------------------------------------- /web/src/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 5 | 73 | 74 | 83 | 84 | 89 | 90 | 97 | -------------------------------------------------------------------------------- /configs/sss-dashboard.yaml.example: -------------------------------------------------------------------------------- 1 | # Simple Server Status Dashboard 配置文件示例 2 | # 使用方法:复制此文件为 sss-dashboard.yaml 并修改配置 3 | 4 | # HTTP 服务配置 5 | port: 8900 # 监听端口,默认 8900 6 | address: 0.0.0.0 # 监听地址,0.0.0.0 表示所有网卡,默认 0.0.0.0 7 | 8 | # WebSocket 路径配置 9 | webSocketPath: /ws-report # WebSocket 路径,建议以 '/' 开头(旧格式 ws-report 会自动兼容) 10 | 11 | # 授权的服务器列表 12 | # 重要:每台服务器必须有唯一的 ID 和密钥 13 | servers: 14 | # 服务器 1 示例 15 | - name: Web Server 1 # 在面板上显示的服务器名称 16 | id: web-server-01 # 服务器唯一ID(3-50个字符,仅允许字母、数字、下划线、连字符) 17 | secret: "YOUR-STRONG-SECRET-KEY-HERE" # 认证密钥,请使用强密钥!建议至少 16 位随机字符 18 | group: production # 服务器分组(可选),用于在面板上分组显示 19 | countryCode: CN # 国家代码(可选,2位字母),不填则根据IP自动识别 20 | 21 | # 服务器 2 示例 22 | - name: Database Server 23 | id: db-server-01 24 | secret: "CHANGE-ME-TO-RANDOM-STRING" 25 | group: production 26 | countryCode: US 27 | 28 | # 服务器 3 示例(最简配置) 29 | - name: Test Server 30 | id: test-01 31 | secret: "ANOTHER-RANDOM-SECRET" 32 | 33 | # 上报时间间隔最大值(可选) 34 | # reportTimeIntervalMax: 30 # 单位:秒,默认 30 秒 35 | 36 | # 日志配置(可选) 37 | # logPath: ./.logs/sss-dashboard.log # 日志文件路径 38 | # logLevel: info # 日志级别:debug, info, warn, error,默认 info 39 | 40 | # =========================================== 41 | # 💡 安全提示 42 | # =========================================== 43 | # 1. 密钥生成建议(Linux/macOS): 44 | # openssl rand -base64 32 45 | # 或 46 | # pwgen -s 32 1 47 | # 48 | # 2. 密钥生成建议(Windows PowerShell): 49 | # -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 32 | % {[char]$_}) 50 | # 51 | # 3. 每台服务器应使用不同的密钥 52 | # 4. 不要使用简单密钥如 "123456"、"password" 等 53 | # 5. Agent 配置中的 serverId 和 authSecret 必须与这里完全一致 54 | # 55 | # =========================================== 56 | # 📖 配置说明 57 | # =========================================== 58 | # 必填项: 59 | # - servers.name: 服务器显示名称 60 | # - servers.id: 服务器唯一标识符 61 | # - servers.secret: 认证密钥 62 | # 63 | # 可选项: 64 | # - port: HTTP 端口 65 | # - address: 监听地址 66 | # - webSocketPath: WebSocket 路径 67 | # - servers.group: 服务器分组 68 | # - servers.countryCode: 国家代码 69 | # - reportTimeIntervalMax: 上报间隔 70 | # - logPath: 日志路径 71 | # - logLevel: 日志级别 72 | # 73 | # 更多文档:https://github.com/ruanun/simple-server-status 74 | -------------------------------------------------------------------------------- /web/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AButton: typeof import('ant-design-vue/es')['Button'] 11 | ACard: typeof import('ant-design-vue/es')['Card'] 12 | ACol: typeof import('ant-design-vue/es')['Col'] 13 | ACollapse: typeof import('ant-design-vue/es')['Collapse'] 14 | ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] 15 | ADropdown: typeof import('ant-design-vue/es')['Dropdown'] 16 | ALayout: typeof import('ant-design-vue/es')['Layout'] 17 | ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] 18 | ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter'] 19 | ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] 20 | AList: typeof import('ant-design-vue/es')['List'] 21 | AListItem: typeof import('ant-design-vue/es')['ListItem'] 22 | AMenu: typeof import('ant-design-vue/es')['Menu'] 23 | AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider'] 24 | AMenuItem: typeof import('ant-design-vue/es')['MenuItem'] 25 | AMenuItemGroup: typeof import('ant-design-vue/es')['MenuItemGroup'] 26 | APopover: typeof import('ant-design-vue/es')['Popover'] 27 | AProgress: typeof import('ant-design-vue/es')['Progress'] 28 | ARow: typeof import('ant-design-vue/es')['Row'] 29 | ATable: typeof import('ant-design-vue/es')['Table'] 30 | ATooltip: typeof import('ant-design-vue/es')['Tooltip'] 31 | ATypographyText: typeof import('ant-design-vue/es')['TypographyText'] 32 | FlagIcon: typeof import('./src/components/FlagIcon.vue')['default'] 33 | HeaderStatus: typeof import('./src/components/HeaderStatus.vue')['default'] 34 | Logo: typeof import('./src/components/Logo.vue')['default'] 35 | ServerInfoContent: typeof import('./src/components/ServerInfoContent.vue')['default'] 36 | ServerInfoExtra: typeof import('./src/components/ServerInfoExtra.vue')['default'] 37 | StatusIndicator: typeof import('./src/components/StatusIndicator.vue')['default'] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/build-dashboard.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Dashboard 完整构建脚本(包含前端) 3 | # 作者: ruan 4 | # 说明: 先构建前端,再构建 Dashboard Go 程序 5 | 6 | set -e # 遇到错误立即退出 7 | 8 | # 颜色定义 9 | GREEN='\033[0;32m' 10 | YELLOW='\033[0;33m' 11 | RED='\033[0;31m' 12 | BLUE='\033[0;34m' 13 | NC='\033[0m' # No Color 14 | 15 | # 获取脚本所在目录的父目录(项目根目录) 16 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 17 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" 18 | 19 | echo -e "${BLUE}================================${NC}" 20 | echo -e "${BLUE} Dashboard 完整构建流程${NC}" 21 | echo -e "${BLUE}================================${NC}" 22 | echo "" 23 | 24 | # 步骤 1: 构建前端 25 | echo -e "${GREEN}📦 步骤 1/2: 构建前端项目${NC}" 26 | echo "" 27 | 28 | if [ -f "$SCRIPT_DIR/build-web.sh" ]; then 29 | bash "$SCRIPT_DIR/build-web.sh" 30 | else 31 | echo -e "${RED}❌ 错误: 未找到 build-web.sh 脚本${NC}" 32 | exit 1 33 | fi 34 | 35 | echo "" 36 | echo -e "${GREEN}✓ 前端构建完成${NC}" 37 | echo "" 38 | 39 | # 步骤 2: 构建 Dashboard Go 程序 40 | echo -e "${GREEN}🔧 步骤 2/2: 构建 Dashboard 二进制文件${NC}" 41 | echo "" 42 | 43 | # 检查 Go 是否安装 44 | if ! command -v go &> /dev/null; then 45 | echo -e "${RED}❌ 错误: 未找到 Go${NC}" 46 | echo -e "${YELLOW}请先安装 Go: https://golang.org/${NC}" 47 | exit 1 48 | fi 49 | 50 | echo -e "${GREEN}✓ Go 版本: $(go version)${NC}" 51 | 52 | # 进入项目根目录 53 | cd "$PROJECT_ROOT" 54 | 55 | # 创建 bin 目录 56 | BIN_DIR="$PROJECT_ROOT/bin" 57 | mkdir -p "$BIN_DIR" 58 | 59 | # 构建 Dashboard 60 | echo -e "${YELLOW}🔨 编译 Dashboard...${NC}" 61 | go build -v -o "$BIN_DIR/sss-dashboard" ./cmd/dashboard 62 | 63 | # 验证构建结果 64 | if [ -f "$BIN_DIR/sss-dashboard" ]; then 65 | echo -e "${GREEN}✅ Dashboard 构建成功!${NC}" 66 | echo "" 67 | echo -e "${BLUE}================================${NC}" 68 | echo -e "${BLUE} 构建完成${NC}" 69 | echo -e "${BLUE}================================${NC}" 70 | echo -e "${GREEN} 二进制文件: $BIN_DIR/sss-dashboard${NC}" 71 | 72 | # 显示文件大小 73 | FILE_SIZE=$(du -h "$BIN_DIR/sss-dashboard" | cut -f1) 74 | echo -e "${GREEN} 文件大小: $FILE_SIZE${NC}" 75 | 76 | # 设置可执行权限 77 | chmod +x "$BIN_DIR/sss-dashboard" 78 | 79 | echo "" 80 | echo -e "${YELLOW}运行方式:${NC}" 81 | echo -e " ${GREEN}./bin/sss-dashboard${NC}" 82 | echo "" 83 | else 84 | echo -e "${RED}❌ 构建失败${NC}" 85 | exit 1 86 | fi 87 | -------------------------------------------------------------------------------- /scripts/build-web.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 前端构建脚本 3 | # 作者: ruan 4 | # 说明: 构建前端项目并复制到 embed 目录 5 | 6 | set -e # 遇到错误立即退出 7 | 8 | # 颜色定义 9 | GREEN='\033[0;32m' 10 | YELLOW='\033[0;33m' 11 | RED='\033[0;31m' 12 | NC='\033[0m' # No Color 13 | 14 | # 获取脚本所在目录的父目录(项目根目录) 15 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 16 | PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" 17 | 18 | echo -e "${GREEN}📦 开始构建前端项目...${NC}" 19 | 20 | # 检查 Node.js 是否安装 21 | if ! command -v node &> /dev/null; then 22 | echo -e "${RED}❌ 错误: 未找到 Node.js${NC}" 23 | echo -e "${YELLOW}请先安装 Node.js: https://nodejs.org/${NC}" 24 | exit 1 25 | fi 26 | 27 | # 检查 pnpm 是否安装 28 | if ! command -v pnpm &> /dev/null; then 29 | echo -e "${RED}❌ 错误: 未找到 pnpm${NC}" 30 | echo -e "${YELLOW}请先安装 pnpm: npm install -g pnpm 或 corepack enable${NC}" 31 | exit 1 32 | fi 33 | 34 | echo -e "${GREEN}✓ Node.js 版本: $(node --version)${NC}" 35 | echo -e "${GREEN}✓ pnpm 版本: $(pnpm --version)${NC}" 36 | 37 | # 进入 web 目录 38 | cd "$PROJECT_ROOT/web" 39 | 40 | # 检查 package.json 是否存在 41 | if [ ! -f "package.json" ]; then 42 | echo -e "${RED}❌ 错误: 未找到 package.json${NC}" 43 | exit 1 44 | fi 45 | 46 | # 安装依赖(仅在 node_modules 不存在时) 47 | if [ ! -d "node_modules" ]; then 48 | echo -e "${YELLOW}📥 安装前端依赖...${NC}" 49 | pnpm install --frozen-lockfile 50 | else 51 | echo -e "${GREEN}✓ 依赖已存在,跳过安装${NC}" 52 | fi 53 | 54 | # 构建前端项目 55 | echo -e "${YELLOW}🔨 构建前端项目(生产模式)...${NC}" 56 | pnpm run build:prod 57 | 58 | # 检查构建产物是否存在 59 | if [ ! -d "dist" ]; then 60 | echo -e "${RED}❌ 错误: 构建失败,未找到 dist 目录${NC}" 61 | exit 1 62 | fi 63 | 64 | # 返回项目根目录 65 | cd "$PROJECT_ROOT" 66 | 67 | # 目标目录 68 | EMBED_DIR="$PROJECT_ROOT/internal/dashboard/public/dist" 69 | 70 | # 创建目标目录(如果不存在) 71 | mkdir -p "$EMBED_DIR" 72 | 73 | # 清空目标目录(保留 .gitkeep 或 README.md) 74 | echo -e "${YELLOW}🗑️ 清理 embed 目录...${NC}" 75 | find "$EMBED_DIR" -mindepth 1 ! -name '.gitkeep' ! -name 'README.md' -delete 76 | 77 | # 复制构建产物 78 | echo -e "${YELLOW}📋 复制构建产物到 embed 目录...${NC}" 79 | cp -r web/dist/* "$EMBED_DIR/" 80 | 81 | # 验证复制结果 82 | if [ -d "$EMBED_DIR/assets" ]; then 83 | echo -e "${GREEN}✅ 前端构建完成!${NC}" 84 | echo -e "${GREEN} 输出目录: $EMBED_DIR${NC}" 85 | 86 | # 显示文件统计 87 | FILE_COUNT=$(find "$EMBED_DIR" -type f | wc -l) 88 | echo -e "${GREEN} 文件数量: $FILE_COUNT${NC}" 89 | else 90 | echo -e "${RED}❌ 错误: 复制失败,未找到 assets 目录${NC}" 91 | exit 1 92 | fi 93 | -------------------------------------------------------------------------------- /internal/dashboard/handler/api.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "sort" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/ruanun/simple-server-status/internal/dashboard/response" 9 | "github.com/ruanun/simple-server-status/pkg/model" 10 | "github.com/samber/lo" 11 | ) 12 | 13 | // WebSocketStatsProvider 定义 WebSocket 统计信息提供者接口 14 | // 用于避免循环导入 internal/dashboard 包 15 | type WebSocketStatsProvider interface { 16 | GetAllServerId() []string 17 | SessionLength() int 18 | } 19 | 20 | // ServerStatusMapProvider 服务器状态 Map 提供者接口 21 | type ServerStatusMapProvider interface { 22 | Count() int 23 | Items() map[string]*model.ServerInfo 24 | } 25 | 26 | // ServerConfigMapProvider 服务器配置 Map 提供者接口 27 | type ServerConfigMapProvider interface { 28 | Count() int 29 | } 30 | 31 | // InitApi 初始化 API 路由 32 | // wsManager: Agent WebSocket 管理器,用于获取连接统计信息 33 | // configProvider: 配置提供者 34 | // logger: 日志记录器 35 | // serverStatusMap: 服务器状态 Map 提供者 36 | // serverConfigMap: 服务器配置 Map 提供者 37 | // configValidator: 配置验证器提供者 38 | func InitApi( 39 | r *gin.Engine, 40 | wsManager WebSocketStatsProvider, 41 | configProvider ConfigProvider, 42 | logger LoggerProvider, 43 | serverStatusMap ServerStatusMapProvider, 44 | serverConfigMap ServerConfigMapProvider, 45 | configValidator ConfigValidatorProvider, 46 | ) { 47 | group := r.Group("/api") 48 | 49 | { 50 | group.GET("/server/statusInfo", StatusInfo(serverStatusMap, configProvider)) 51 | //统计信息 52 | group.GET("/statistics", func(c *gin.Context) { 53 | response.Success(c, gin.H{ 54 | "onlineIds": wsManager.GetAllServerId(), 55 | "sessionMapLen": wsManager.SessionLength(), 56 | "reportMapLen": serverStatusMap.Count(), 57 | "configServersLen": serverConfigMap.Count(), 58 | }) 59 | }) 60 | 61 | // 初始化配置相关API TODO 暂不使用 62 | //InitConfigAPI(group, configProvider, logger, configValidator) 63 | 64 | } 65 | } 66 | 67 | // StatusInfo 获取服务器状态信息(工厂函数) 68 | func StatusInfo(serverStatusMap ServerStatusMapProvider, configProvider ConfigProvider) gin.HandlerFunc { 69 | return func(c *gin.Context) { 70 | // 处理数据结构并返回 71 | values := lo.Values(serverStatusMap.Items()) 72 | //转换 73 | baseServerInfos := lo.Map(values, func(item *model.ServerInfo, index int) *model.RespServerInfo { 74 | info := model.NewRespServerInfo(item) 75 | isOnline := time.Now().Unix()-info.LastReportTime <= int64(configProvider.GetConfig().ReportTimeIntervalMax) 76 | info.IsOnline = isOnline 77 | return info 78 | }) 79 | sort.Slice(baseServerInfos, func(i, j int) bool { 80 | return baseServerInfos[i].Id < baseServerInfos[j].Id 81 | }) 82 | response.Success(c, baseServerInfos) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /web/src/stores/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 服务器数据状态管理 Store 3 | * 4 | * 职责: 5 | * - 管理服务器数据的存储和更新 6 | * - 提供服务器分组和统计信息 7 | * - 支持 HTTP 轮询和 WebSocket 两种数据源 8 | * 9 | * 使用方式: 10 | * ```typescript 11 | * import { useServerStore } from '@/stores/server' 12 | * import { storeToRefs } from 'pinia' 13 | * const serverStore = useServerStore() 14 | * 15 | * // 解构响应式数据(必须使用 storeToRefs) 16 | * const { groupedServers, groupNames, totalCount, onlineCount } = storeToRefs(serverStore) 17 | * 18 | * // 更新数据(HTTP 轮询和 WebSocket 统一使用) 19 | * serverStore.setServerData(serverInfoArray) 20 | * ``` 21 | * 22 | * @author ruan 23 | */ 24 | 25 | import { defineStore } from 'pinia' 26 | import { ref, computed } from 'vue' 27 | import type { ServerInfo } from '@/api/models' 28 | 29 | /** 30 | * 服务器数据 Store 31 | * 管理服务器列表、分组和统计信息 32 | */ 33 | export const useServerStore = defineStore('server', () => { 34 | // ==================== 状态 ==================== 35 | 36 | /** 37 | * 服务器数据原始存储 38 | * key: 分组名称 39 | * value: 该分组下的服务器列表 40 | */ 41 | const serverData = ref>(new Map()) 42 | 43 | // ==================== Getters ==================== 44 | 45 | /** 46 | * 分组后的服务器数据 47 | */ 48 | const groupedServers = computed(() => serverData.value) 49 | 50 | /** 51 | * 所有分组名称列表 52 | */ 53 | const groupNames = computed(() => { 54 | return Array.from(serverData.value.keys()) 55 | }) 56 | 57 | /** 58 | * 服务器总数 59 | */ 60 | const totalCount = computed(() => { 61 | let total = 0 62 | serverData.value.forEach(servers => { 63 | total += servers.length 64 | }) 65 | return total 66 | }) 67 | 68 | /** 69 | * 在线服务器数量 70 | */ 71 | const onlineCount = computed(() => { 72 | let online = 0 73 | serverData.value.forEach(servers => { 74 | online += servers.filter(server => server.isOnline).length 75 | }) 76 | return online 77 | }) 78 | 79 | // ==================== Actions ==================== 80 | 81 | /** 82 | * 设置服务器数据(HTTP 轮询和 WebSocket 统一使用) 83 | * @param data 服务器信息数组 84 | */ 85 | function setServerData(data: ServerInfo[]) { 86 | const map = new Map() 87 | 88 | data.forEach(item => { 89 | if (!map.has(item.group)) { 90 | map.set(item.group, []) 91 | } 92 | map.get(item.group)?.push(item) 93 | }) 94 | 95 | serverData.value = map 96 | } 97 | 98 | return { 99 | // 状态 100 | serverData, 101 | // Getters 102 | groupedServers, 103 | groupNames, 104 | totalCount, 105 | onlineCount, 106 | // Actions 107 | setServerData 108 | } 109 | }) 110 | -------------------------------------------------------------------------------- /internal/agent/network_stats.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/ruanun/simple-server-status/pkg/model" 9 | 10 | "github.com/shirou/gopsutil/v4/net" 11 | ) 12 | 13 | // NetworkStatsCollector 线程安全的网络统计收集器 14 | type NetworkStatsCollector struct { 15 | mu sync.RWMutex 16 | netInSpeed uint64 17 | netOutSpeed uint64 18 | netInTransfer uint64 19 | netOutTransfer uint64 20 | lastUpdateNetStats uint64 21 | excludeInterfaces []string 22 | } 23 | 24 | // NewNetworkStatsCollector 创建网络统计收集器 25 | func NewNetworkStatsCollector(excludeInterfaces []string) *NetworkStatsCollector { 26 | if excludeInterfaces == nil { 27 | excludeInterfaces = []string{ 28 | "lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube", 29 | } 30 | } 31 | return &NetworkStatsCollector{ 32 | excludeInterfaces: excludeInterfaces, 33 | } 34 | } 35 | 36 | // Update 更新网络统计(在单独的 goroutine 中调用) 37 | func (nsc *NetworkStatsCollector) Update() error { 38 | netIOs, err := net.IOCounters(true) 39 | if err != nil { 40 | return fmt.Errorf("获取网络IO统计失败: %w", err) 41 | } 42 | 43 | var innerNetInTransfer, innerNetOutTransfer uint64 44 | for _, v := range netIOs { 45 | if isListContainsStr(nsc.excludeInterfaces, v.Name) { 46 | continue 47 | } 48 | innerNetInTransfer += v.BytesRecv 49 | innerNetOutTransfer += v.BytesSent 50 | } 51 | 52 | // 获取当前时间戳并安全转换为 uint64 53 | timestamp := time.Now().Unix() 54 | if timestamp < 0 { 55 | // 理论上不会发生(Unix时间戳始终为正),但为安全起见进行检查 56 | timestamp = 0 57 | } 58 | //nolint:gosec // G115: 已在上方进行负数检查,转换安全 59 | now := uint64(timestamp) 60 | 61 | // 使用写锁保护并发写入 62 | nsc.mu.Lock() 63 | defer nsc.mu.Unlock() 64 | 65 | diff := now - nsc.lastUpdateNetStats 66 | if diff > 0 { 67 | // 检测计数器回绕或网络接口重置 68 | if innerNetInTransfer >= nsc.netInTransfer { 69 | nsc.netInSpeed = (innerNetInTransfer - nsc.netInTransfer) / diff 70 | } else { 71 | // 发生回绕或重置,从新值开始计算 72 | nsc.netInSpeed = 0 73 | } 74 | 75 | if innerNetOutTransfer >= nsc.netOutTransfer { 76 | nsc.netOutSpeed = (innerNetOutTransfer - nsc.netOutTransfer) / diff 77 | } else { 78 | // 发生回绕或重置,从新值开始计算 79 | nsc.netOutSpeed = 0 80 | } 81 | } 82 | nsc.netInTransfer = innerNetInTransfer 83 | nsc.netOutTransfer = innerNetOutTransfer 84 | nsc.lastUpdateNetStats = now 85 | 86 | return nil 87 | } 88 | 89 | // GetStats 获取当前网络统计(线程安全) 90 | func (nsc *NetworkStatsCollector) GetStats() *model.NetworkInfo { 91 | // 使用读锁允许并发读取 92 | nsc.mu.RLock() 93 | defer nsc.mu.RUnlock() 94 | 95 | return &model.NetworkInfo{ 96 | NetInSpeed: nsc.netInSpeed, 97 | NetOutSpeed: nsc.netOutSpeed, 98 | NetInTransfer: nsc.netInTransfer, 99 | NetOutTransfer: nsc.netOutTransfer, 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /web/src/stores/connection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 连接状态管理 Store 3 | * 4 | * 职责: 5 | * - 管理 WebSocket/HTTP 连接模式切换 6 | * - 提供连接状态和统计信息的响应式访问 7 | * - 处理重连逻辑 8 | * 9 | * 使用方式: 10 | * ```typescript 11 | * import { useConnectionStore } from '@/stores/connection' 12 | * const connectionStore = useConnectionStore() 13 | * 14 | * // 读取状态 15 | * console.log(connectionStore.mode) // 'websocket' | 'http' 16 | * console.log(connectionStore.isWebSocketMode) 17 | * 18 | * // 切换模式 19 | * connectionStore.toggleMode() 20 | * 21 | * // 重连 22 | * await connectionStore.reconnect() 23 | * ``` 24 | * 25 | * @author ruan 26 | */ 27 | 28 | import { defineStore } from 'pinia' 29 | import { ref, computed } from 'vue' 30 | import { useWebSocket } from '@/api/websocket' 31 | import { CONNECTION_MODES, type ConnectionMode } from '@/constants/connectionModes' 32 | 33 | // 获取 WebSocket 实例 34 | const ws = useWebSocket() 35 | 36 | /** 37 | * 连接状态 Store 38 | * 管理 WebSocket/HTTP 连接模式和状态 39 | */ 40 | export const useConnectionStore = defineStore('connection', () => { 41 | // ==================== WebSocket 响应式状态 ==================== 42 | 43 | /** 44 | * WebSocket 连接状态(直接引用全局实例) 45 | */ 46 | const status = ws.status 47 | 48 | /** 49 | * WebSocket 连接统计信息(直接引用全局实例) 50 | */ 51 | const connectionStats = ws.connectionStats 52 | 53 | // ==================== 状态 ==================== 54 | 55 | /** 56 | * 当前连接模式 57 | * - websocket: WebSocket 实时模式 58 | * - http: HTTP 轮询模式 59 | */ 60 | const mode = ref(CONNECTION_MODES.WEBSOCKET) 61 | 62 | // ==================== Getters ==================== 63 | 64 | /** 65 | * WebSocket 连接状态 66 | */ 67 | const websocketStatus = computed(() => status.value) 68 | 69 | /** 70 | * WebSocket 连接统计信息 71 | */ 72 | const stats = computed(() => connectionStats) 73 | 74 | /** 75 | * 是否为 WebSocket 模式 76 | */ 77 | const isWebSocketMode = computed(() => mode.value === CONNECTION_MODES.WEBSOCKET) 78 | 79 | // ==================== Actions ==================== 80 | 81 | /** 82 | * 切换连接模式(WebSocket ↔ HTTP) 83 | * 注意:此方法仅切换模式状态,实际的连接/断开逻辑由组件监听状态变化后执行 84 | */ 85 | function toggleMode() { 86 | mode.value = mode.value === CONNECTION_MODES.WEBSOCKET 87 | ? CONNECTION_MODES.HTTP 88 | : CONNECTION_MODES.WEBSOCKET 89 | } 90 | 91 | /** 92 | * 设置连接模式 93 | * @param newMode 新的连接模式 94 | */ 95 | function setMode(newMode: ConnectionMode) { 96 | mode.value = newMode 97 | } 98 | 99 | /** 100 | * 重连 WebSocket 101 | * @returns Promise,resolve 表示成功,reject 表示失败 102 | */ 103 | async function reconnect() { 104 | return await ws.connect() 105 | } 106 | 107 | return { 108 | // 状态 109 | mode, 110 | // Getters 111 | websocketStatus, 112 | stats, 113 | isWebSocketMode, 114 | // Actions 115 | toggleMode, 116 | setMode, 117 | reconnect 118 | } 119 | }) 120 | -------------------------------------------------------------------------------- /web/src/utils/colorUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 颜色工具函数 3 | * 4 | * 职责: 5 | * - 定义百分比阈值和颜色常量 6 | * - 提供根据百分比返回颜色的工具函数 7 | * - 统一百分比颜色判断逻辑 8 | * 9 | * @author ruan 10 | */ 11 | 12 | /** 13 | * 百分比阈值常量 14 | * 用于判断状态的临界点 15 | */ 16 | export const PERCENT_THRESHOLDS = { 17 | /** 危险阈值 (>= 90%) */ 18 | critical: 90, 19 | /** 警告阈值 (>= 70%) */ 20 | warning: 70 21 | } as const 22 | 23 | /** 24 | * 百分比状态颜色常量 25 | * 基于 Ant Design 的色彩规范 26 | */ 27 | export const PERCENT_COLORS = { 28 | /** 危险状态 - 红色 */ 29 | critical: '#ff4d4f', 30 | /** 警告状态 - 橙色 */ 31 | warning: '#faad14', 32 | /** 正常状态 - 绿色 */ 33 | normal: '#52c41a', 34 | /** 默认状态 - 空字符串(使用组件默认色) */ 35 | default: '' 36 | } as const 37 | 38 | /** 39 | * 百分比状态 CSS 类名 40 | */ 41 | export const PERCENT_CLASSES = { 42 | critical: 'critical', 43 | warning: 'warning', 44 | normal: 'normal' 45 | } as const 46 | 47 | /** 48 | * 根据百分比返回对应的颜色 49 | * @param percent 百分比值 (0-100) 50 | * @param useDefault 是否在正常状态下返回空字符串(默认:true,兼容 Ant Design Progress 组件) 51 | * @returns 颜色值 52 | * 53 | * @example 54 | * ```typescript 55 | * getPercentColor(95) // '#ff4d4f' (红色) 56 | * getPercentColor(75) // '#faad14' (橙色) 57 | * getPercentColor(50) // '' (默认) 58 | * getPercentColor(50, false) // '#52c41a' (绿色) 59 | * ``` 60 | */ 61 | export function getPercentColor(percent: number, useDefault: boolean = true): string { 62 | if (percent >= PERCENT_THRESHOLDS.critical) { 63 | return PERCENT_COLORS.critical 64 | } 65 | if (percent >= PERCENT_THRESHOLDS.warning) { 66 | return PERCENT_COLORS.warning 67 | } 68 | return useDefault ? PERCENT_COLORS.default : PERCENT_COLORS.normal 69 | } 70 | 71 | /** 72 | * 根据百分比返回对应的 CSS 类名 73 | * @param percent 百分比值 (0-100) 74 | * @returns CSS 类名 75 | * 76 | * @example 77 | * ```typescript 78 | * getPercentClass(95) // 'critical' 79 | * getPercentClass(75) // 'warning' 80 | * getPercentClass(50) // 'normal' 81 | * ``` 82 | */ 83 | export function getPercentClass(percent: number): string { 84 | if (percent >= PERCENT_THRESHOLDS.critical) { 85 | return PERCENT_CLASSES.critical 86 | } 87 | if (percent >= PERCENT_THRESHOLDS.warning) { 88 | return PERCENT_CLASSES.warning 89 | } 90 | return PERCENT_CLASSES.normal 91 | } 92 | 93 | /** 94 | * 判断百分比是否处于危险状态 95 | * @param percent 百分比值 (0-100) 96 | * @returns 是否危险 97 | */ 98 | export function isCriticalPercent(percent: number): boolean { 99 | return percent >= PERCENT_THRESHOLDS.critical 100 | } 101 | 102 | /** 103 | * 判断百分比是否处于警告状态 104 | * @param percent 百分比值 (0-100) 105 | * @returns 是否警告 106 | */ 107 | export function isWarningPercent(percent: number): boolean { 108 | return percent >= PERCENT_THRESHOLDS.warning && percent < PERCENT_THRESHOLDS.critical 109 | } 110 | 111 | /** 112 | * 判断百分比是否处于正常状态 113 | * @param percent 百分比值 (0-100) 114 | * @returns 是否正常 115 | */ 116 | export function isNormalPercent(percent: number): boolean { 117 | return percent < PERCENT_THRESHOLDS.warning 118 | } 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ruanun/simple-server-status 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.8.0 7 | github.com/gin-contrib/static v1.1.2 8 | github.com/gin-contrib/zap v1.1.4 9 | github.com/gin-gonic/gin v1.10.0 10 | github.com/go-playground/validator/v10 v10.23.0 11 | github.com/gorilla/websocket v1.5.3 12 | github.com/olahol/melody v1.2.1 13 | github.com/orcaman/concurrent-map/v2 v2.0.1 14 | github.com/samber/lo v1.47.0 15 | github.com/shirou/gopsutil/v4 v4.24.11 16 | github.com/spf13/viper v1.19.0 17 | go.uber.org/zap v1.27.0 18 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 19 | ) 20 | 21 | require ( 22 | github.com/bytedance/sonic v1.12.1 // indirect 23 | github.com/bytedance/sonic/loader v0.2.0 // indirect 24 | github.com/cloudwego/base64x v0.1.4 // indirect 25 | github.com/cloudwego/iasm v0.2.0 // indirect 26 | github.com/ebitengine/purego v0.8.1 // indirect 27 | github.com/gabriel-vasile/mimetype v1.4.7 // indirect 28 | github.com/gin-contrib/sse v0.1.0 // indirect 29 | github.com/go-ole/go-ole v1.2.6 // indirect 30 | github.com/go-playground/locales v0.14.1 // indirect 31 | github.com/go-playground/universal-translator v0.18.1 // indirect 32 | github.com/goccy/go-json v0.10.3 // indirect 33 | github.com/hashicorp/hcl v1.0.0 // indirect 34 | github.com/json-iterator/go v1.1.12 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 36 | github.com/leodido/go-urn v1.4.0 // indirect 37 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 38 | github.com/magiconair/properties v1.8.9 // indirect 39 | github.com/mattn/go-isatty v0.0.20 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 44 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 45 | github.com/sagikazarmark/locafero v0.6.0 // indirect 46 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 47 | github.com/sourcegraph/conc v0.3.0 // indirect 48 | github.com/spf13/afero v1.11.0 // indirect 49 | github.com/spf13/cast v1.7.1 // indirect 50 | github.com/spf13/pflag v1.0.5 // indirect 51 | github.com/subosito/gotenv v1.6.0 // indirect 52 | github.com/tklauser/go-sysconf v0.3.12 // indirect 53 | github.com/tklauser/numcpus v0.6.1 // indirect 54 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 55 | github.com/ugorji/go/codec v1.2.12 // indirect 56 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 57 | go.uber.org/multierr v1.11.0 // indirect 58 | golang.org/x/arch v0.9.0 // indirect 59 | golang.org/x/crypto v0.31.0 // indirect 60 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 61 | golang.org/x/net v0.33.0 // indirect 62 | golang.org/x/sys v0.28.0 // indirect 63 | golang.org/x/text v0.21.0 // indirect 64 | google.golang.org/protobuf v1.34.2 // indirect 65 | gopkg.in/ini.v1 v1.67.0 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /internal/shared/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "os" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | "go.uber.org/zap/zapcore" 9 | "gopkg.in/natefinch/lumberjack.v2" 10 | ) 11 | 12 | // Config 日志配置 13 | type Config struct { 14 | Level string // 日志级别: debug, info, warn, error, dpanic, panic, fatal 15 | FilePath string // 日志文件路径 16 | MaxSize int // 单个日志文件最大大小(MB) 17 | MaxAge int // 日志文件保留天数 18 | Compress bool // 是否压缩旧日志 19 | LocalTime bool // 是否使用本地时间 20 | } 21 | 22 | // DefaultConfig 返回默认日志配置 23 | func DefaultConfig() Config { 24 | return Config{ 25 | Level: "info", 26 | FilePath: "", 27 | MaxSize: 64, 28 | MaxAge: 5, 29 | Compress: false, 30 | LocalTime: true, 31 | } 32 | } 33 | 34 | // LevelMap 日志级别映射 35 | var LevelMap = map[string]zapcore.Level{ 36 | "debug": zapcore.DebugLevel, 37 | "info": zapcore.InfoLevel, 38 | "warn": zapcore.WarnLevel, 39 | "error": zapcore.ErrorLevel, 40 | "dpanic": zapcore.DPanicLevel, 41 | "panic": zapcore.PanicLevel, 42 | "fatal": zapcore.FatalLevel, 43 | } 44 | 45 | // New 创建新的日志实例 46 | func New(cfg Config) (*zap.SugaredLogger, error) { 47 | // 解析日志级别 48 | level, ok := LevelMap[cfg.Level] 49 | if !ok { 50 | level = zapcore.InfoLevel 51 | } 52 | 53 | atomicLevel := zap.NewAtomicLevelAt(level) 54 | core := zapcore.NewCore( 55 | getEncoder(), 56 | getLogWriter(cfg), 57 | atomicLevel, 58 | ) 59 | 60 | logger := zap.New(core, zap.AddCaller()) 61 | sugaredLogger := logger.Sugar() 62 | 63 | sugaredLogger.Infof("日志模块初始化成功 [level=%s, file=%s]", cfg.Level, cfg.FilePath) 64 | return sugaredLogger, nil 65 | } 66 | 67 | // getLogWriter 获取日志输出器 68 | func getLogWriter(cfg Config) zapcore.WriteSyncer { 69 | writers := []zapcore.WriteSyncer{ 70 | zapcore.AddSync(os.Stdout), // 始终输出到控制台 71 | } 72 | 73 | // 如果指定了日志文件路径,则同时输出到文件 74 | if cfg.FilePath != "" { 75 | writers = append(writers, zapcore.AddSync(&lumberjack.Logger{ 76 | Filename: cfg.FilePath, 77 | MaxSize: cfg.MaxSize, 78 | MaxAge: cfg.MaxAge, 79 | LocalTime: cfg.LocalTime, 80 | Compress: cfg.Compress, 81 | })) 82 | } 83 | 84 | return zapcore.NewMultiWriteSyncer(writers...) 85 | } 86 | 87 | // getEncoder 获取日志编码器 88 | func getEncoder() zapcore.Encoder { 89 | // 自定义时间格式 90 | customTimeEncoder := func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 91 | enc.AppendString(t.Format("2006-01-02 15:04:05.000")) 92 | } 93 | 94 | // 自定义代码路径、行号输出 95 | customCallerEncoder := func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { 96 | enc.AppendString("[" + caller.TrimmedPath() + "]") 97 | } 98 | 99 | encoderConfig := zap.NewProductionEncoderConfig() 100 | encoderConfig.EncodeTime = customTimeEncoder 101 | encoderConfig.TimeKey = "time" 102 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 103 | encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder 104 | encoderConfig.EncodeCaller = customCallerEncoder 105 | 106 | return zapcore.NewConsoleEncoder(encoderConfig) 107 | } 108 | -------------------------------------------------------------------------------- /internal/agent/mempool.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "sync" 7 | ) 8 | 9 | // BufferPool 字节缓冲池 10 | type BufferPool struct { 11 | pool sync.Pool 12 | } 13 | 14 | // NewBufferPool 创建新的缓冲池 15 | func NewBufferPool() *BufferPool { 16 | return &BufferPool{ 17 | pool: sync.Pool{ 18 | New: func() interface{} { 19 | return &bytes.Buffer{} 20 | }, 21 | }, 22 | } 23 | } 24 | 25 | // Get 从池中获取缓冲区 26 | func (bp *BufferPool) Get() *bytes.Buffer { 27 | buf := bp.pool.Get().(*bytes.Buffer) 28 | buf.Reset() 29 | return buf 30 | } 31 | 32 | // Put 将缓冲区放回池中 33 | func (bp *BufferPool) Put(buf *bytes.Buffer) { 34 | // 如果缓冲区太大,不放回池中,避免内存泄漏 35 | if buf.Cap() > 64*1024 { // 64KB 36 | return 37 | } 38 | bp.pool.Put(buf) 39 | } 40 | 41 | // MemoryPoolManager 内存池管理器 42 | // 注意: json.Encoder 不适合池化,因为它绑定了特定的 io.Writer 43 | // 我们只池化 bytes.Buffer,每次创建新的 Encoder 44 | type MemoryPoolManager struct { 45 | bufferPool *BufferPool 46 | stats PoolStats 47 | mu sync.RWMutex 48 | } 49 | 50 | // NewMemoryPoolManager 创建新的内存池管理器 51 | func NewMemoryPoolManager() *MemoryPoolManager { 52 | return &MemoryPoolManager{ 53 | bufferPool: NewBufferPool(), 54 | } 55 | } 56 | 57 | // PoolStats 池统计信息 58 | type PoolStats struct { 59 | BufferGets int64 60 | BufferPuts int64 61 | MemorySaved int64 // 估算节省的内存分配次数 62 | } 63 | 64 | // GetBuffer 获取缓冲区 65 | func (mpm *MemoryPoolManager) GetBuffer() *bytes.Buffer { 66 | mpm.mu.Lock() 67 | mpm.stats.BufferGets++ 68 | mpm.mu.Unlock() 69 | return mpm.bufferPool.Get() 70 | } 71 | 72 | // PutBuffer 归还缓冲区 73 | func (mpm *MemoryPoolManager) PutBuffer(buf *bytes.Buffer) { 74 | mpm.mu.Lock() 75 | mpm.stats.BufferPuts++ 76 | mpm.stats.MemorySaved++ 77 | mpm.mu.Unlock() 78 | mpm.bufferPool.Put(buf) 79 | } 80 | 81 | // GetStats 获取池统计信息 82 | func (mpm *MemoryPoolManager) GetStats() PoolStats { 83 | mpm.mu.RLock() 84 | defer mpm.mu.RUnlock() 85 | return mpm.stats 86 | } 87 | 88 | // LogStats 记录池统计信息 89 | func (mpm *MemoryPoolManager) LogStats(logger interface{ Infof(string, ...interface{}) }) { 90 | if logger == nil { 91 | return 92 | } 93 | stats := mpm.GetStats() 94 | logger.Infof("Memory Pool Stats - Buffer Gets: %d, Puts: %d, Memory Saved: %d", 95 | stats.BufferGets, stats.BufferPuts, stats.MemorySaved) 96 | } 97 | 98 | // OptimizedJSONMarshal 使用内存池的优化JSON序列化 99 | // 只池化 bytes.Buffer,每次创建新的 json.Encoder 100 | func (mpm *MemoryPoolManager) OptimizedJSONMarshal(v interface{}) ([]byte, error) { 101 | // 从池中获取 buffer 102 | buf := mpm.GetBuffer() 103 | defer mpm.PutBuffer(buf) 104 | 105 | // 每次创建新的 encoder 使用池化的 buffer 106 | encoder := json.NewEncoder(buf) 107 | err := encoder.Encode(v) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | // 移除最后的换行符(Encode 会添加) 113 | data := buf.Bytes() 114 | if len(data) > 0 && data[len(data)-1] == '\n' { 115 | data = data[:len(data)-1] 116 | } 117 | 118 | // 复制数据,因为 buf 会被重用 119 | result := make([]byte, len(data)) 120 | copy(result, data) 121 | return result, nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/model/ServerStatusInfo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/shirou/gopsutil/v4/load" 4 | 5 | type ServerInfo struct { 6 | Name string `json:"name"` //name展示 7 | Group string `json:"group"` //组 8 | Id string `json:"id"` //服务器id 9 | LastReportTime int64 `json:"lastReportTime"` //最后上报时间 10 | 11 | HostInfo *HostInfo `json:"hostInfo"` 12 | CpuInfo *CpuInfo `json:"cpuInfo"` 13 | VirtualMemoryInfo *VirtualMemoryInfo `json:"virtualMemoryInfo"` 14 | SwapMemoryInfo *SwapMemoryInfo `json:"swapMemoryInfo"` 15 | DiskInfo *DiskInfo `json:"diskInfo"` 16 | NetworkInfo *NetworkInfo `json:"networkInfo"` 17 | 18 | Ip string `json:"ip"` 19 | Loc string `json:"loc"` 20 | } 21 | type CpuInfo struct { 22 | //Cores int32 `json:"cores"` 23 | //ModelName string `json:"modelName"` 24 | //Mhz float64 `json:"mhz"` 25 | /*cpu占用*/ 26 | Percent float64 `json:"percent"` 27 | //cpu信息字符串描述 28 | Info []string `json:"info"` 29 | } 30 | type HostInfo struct { 31 | KernelArch string `json:"kernelArch"` // native cpu architecture queried at runtime, as returned by `uname -m` or empty string in case of error 32 | KernelVersion string `json:"kernelVersion"` 33 | VirtualizationSystem string `json:"virtualizationSystem"` 34 | Uptime uint64 `json:"uptime"` //单位秒 35 | BootTime uint64 `json:"bootTime"` 36 | //Procs uint64 `json:"procs"` // number of processes 37 | OS string `json:"os"` // ex: freebsd, linux 38 | Platform string `json:"platform"` // ex: ubuntu, linuxmint 39 | PlatformFamily string `json:"platformFamily"` // ex: debian, rhel 40 | PlatformVersion string `json:"platformVersion"` //具体版本 41 | AvgStat *load.AvgStat `json:"avgStat"` 42 | } 43 | type VirtualMemoryInfo struct { 44 | Total uint64 `json:"total"` 45 | //Available uint64 `json:"available"` 46 | Used uint64 `json:"used"` 47 | UsedPercent float64 `json:"usedPercent"` 48 | //Free uint64 `json:"free"` 49 | } 50 | type SwapMemoryInfo struct { 51 | Total uint64 `json:"total"` 52 | Used uint64 `json:"used"` 53 | Free uint64 `json:"free"` 54 | UsedPercent float64 `json:"usedPercent"` 55 | } 56 | type DiskInfo struct { 57 | Total uint64 `json:"total"` 58 | Used uint64 `json:"used"` 59 | UsedPercent float64 `json:"usedPercent"` 60 | Partitions []*Partition `json:"partitions"` 61 | } 62 | 63 | // Partition /*磁盘分区信息*/ 64 | type Partition struct { 65 | MountPoint string `json:"mountPoint"` 66 | Fstype string `json:"fstype"` 67 | Total uint64 `json:"total"` 68 | Free uint64 `json:"free"` 69 | Used uint64 `json:"used"` 70 | UsedPercent float64 `json:"usedPercent"` 71 | } 72 | type NetworkInfo struct { 73 | //下载速度 74 | NetInSpeed uint64 `json:"netInSpeed"` 75 | //上传速度 76 | NetOutSpeed uint64 `json:"netOutSpeed"` 77 | //下载 78 | NetInTransfer uint64 `json:"netInTransfer"` 79 | //上传 80 | NetOutTransfer uint64 `json:"netOutTransfer"` 81 | } 82 | -------------------------------------------------------------------------------- /cmd/agent/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | internal "github.com/ruanun/simple-server-status/internal/agent" 8 | "github.com/ruanun/simple-server-status/internal/agent/config" 9 | "github.com/ruanun/simple-server-status/internal/agent/global" 10 | "github.com/ruanun/simple-server-status/internal/shared/app" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | func main() { 15 | // 创建应用 16 | application := app.New("SSS-Agent", app.BuildInfo{ 17 | GitCommit: global.GitCommit, 18 | Version: global.Version, 19 | BuiltAt: global.BuiltAt, 20 | GoVersion: global.GoVersion, 21 | }) 22 | 23 | // 打印构建信息 24 | application.PrintBuildInfo() 25 | 26 | // 运行应用 27 | if err := run(application); err != nil { 28 | panic(fmt.Errorf("应用启动失败: %v", err)) 29 | } 30 | 31 | // 等待关闭信号 32 | application.WaitForShutdown() 33 | } 34 | 35 | func run(application *app.Application) error { 36 | // 1. 加载配置 37 | cfg, err := loadConfig() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // 2. 初始化日志 43 | logger, err := initLogger(cfg) 44 | if err != nil { 45 | return err 46 | } 47 | application.SetLogger(logger) 48 | application.RegisterCleanup(func() error { 49 | return logger.Sync() 50 | }) 51 | 52 | // 3. 环境验证 53 | if err := internal.ValidateEnvironment(); err != nil { 54 | logger.Warnf("环境验证警告: %v", err) 55 | } 56 | 57 | // 4. 创建并启动 Agent 服务 58 | agentService, err := internal.NewAgentService(cfg, logger) 59 | if err != nil { 60 | return fmt.Errorf("创建 Agent 服务失败: %w", err) 61 | } 62 | 63 | // 启动服务 64 | if err := agentService.Start(); err != nil { 65 | return fmt.Errorf("启动 Agent 服务失败: %w", err) 66 | } 67 | 68 | // 5. 注册清理函数 69 | registerCleanups(application, agentService) 70 | 71 | return nil 72 | } 73 | 74 | func loadConfig() (*config.AgentConfig, error) { 75 | // 使用闭包捕获配置指针以支持热加载 76 | var currentCfg *config.AgentConfig 77 | 78 | cfg, err := app.LoadConfig[*config.AgentConfig]( 79 | "sss-agent.yaml", 80 | "yaml", 81 | []string{".", "./configs", "/etc/sssa", "/etc/sss"}, 82 | true, 83 | func(newCfg *config.AgentConfig) error { 84 | // 热加载回调:更新已返回配置对象的内容 85 | if currentCfg != nil { 86 | // 验证和设置默认值 87 | if err := internal.ValidateAndSetDefaults(newCfg); err != nil { 88 | return fmt.Errorf("热加载配置验证失败: %w", err) 89 | } 90 | *currentCfg = *newCfg 91 | fmt.Println("[INFO] Agent 配置已热加载") 92 | } 93 | return nil 94 | }, 95 | ) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | // 保存配置指针供闭包使用 101 | currentCfg = cfg 102 | 103 | // 详细验证和设置默认值 104 | if err := internal.ValidateAndSetDefaults(cfg); err != nil { 105 | return nil, fmt.Errorf("配置验证失败: %w", err) 106 | } 107 | 108 | return cfg, nil 109 | } 110 | 111 | func initLogger(cfg *config.AgentConfig) (*zap.SugaredLogger, error) { 112 | return app.InitLogger(cfg.LogLevel, cfg.LogPath) 113 | } 114 | 115 | func registerCleanups(application *app.Application, agentService *internal.AgentService) { 116 | // 注册 Agent 服务清理 117 | // 设置 10 秒超时用于优雅关闭 118 | application.RegisterCleanup(func() error { 119 | return agentService.Stop(10 * time.Second) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /internal/shared/config/loader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/fsnotify/fsnotify" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // LoadOptions 配置加载选项 12 | type LoadOptions struct { 13 | ConfigName string // 配置文件名(默认值) 14 | ConfigType string // 配置文件类型: yaml, json, toml 等 15 | ConfigEnvKey string // 环境变量名(用于覆盖配置文件路径) 16 | SearchPaths []string // 配置文件搜索路径 17 | OnConfigChange func(*viper.Viper) error // 配置变更回调 18 | WatchConfigFile bool // 是否监听配置文件变更 19 | } 20 | 21 | // DefaultLoadOptions 返回默认配置加载选项 22 | func DefaultLoadOptions(configName string) LoadOptions { 23 | return LoadOptions{ 24 | ConfigName: configName, 25 | ConfigType: "yaml", 26 | ConfigEnvKey: "CONFIG", 27 | SearchPaths: []string{".", "./configs", "/etc/sss"}, 28 | WatchConfigFile: true, 29 | } 30 | } 31 | 32 | // Load 加载配置文件 33 | // 优先级: 命令行 -c 参数 > 环境变量 > 搜索路径 34 | func Load(opts LoadOptions, cfg interface{}) (*viper.Viper, error) { 35 | configFile, err := resolveConfigPath(opts) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | v := viper.New() 41 | v.SetConfigFile(configFile) 42 | v.SetConfigType(opts.ConfigType) 43 | 44 | // 读取配置文件 45 | if err := v.ReadInConfig(); err != nil { 46 | if os.IsNotExist(err) { 47 | return nil, fmt.Errorf("配置文件不存在: %s", configFile) 48 | } 49 | return nil, fmt.Errorf("读取配置文件失败: %w", err) 50 | } 51 | 52 | // 解析配置到结构体 53 | if err := v.Unmarshal(cfg); err != nil { 54 | return nil, fmt.Errorf("解析配置文件失败: %w", err) 55 | } 56 | 57 | // 监听配置文件变更 58 | if opts.WatchConfigFile { 59 | v.WatchConfig() 60 | if opts.OnConfigChange != nil { 61 | v.OnConfigChange(func(e fsnotify.Event) { 62 | fmt.Printf("配置文件已变更: %s\n", e.Name) 63 | // 由回调函数自己处理 Unmarshal 和验证,确保原子性 64 | if err := opts.OnConfigChange(v); err != nil { 65 | fmt.Printf("配置变更处理失败: %v\n", err) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | fmt.Printf("配置加载成功: %s\n", configFile) 72 | return v, nil 73 | } 74 | 75 | // resolveConfigPath 解析配置文件路径 76 | // 优先级: 环境变量 > 默认搜索路径 77 | func resolveConfigPath(opts LoadOptions) (string, error) { 78 | // 1. 优先使用环境变量 79 | if opts.ConfigEnvKey != "" { 80 | if configPath := os.Getenv(opts.ConfigEnvKey); configPath != "" { 81 | fmt.Printf("使用环境变量 %s 指定的配置文件: %s\n", opts.ConfigEnvKey, configPath) 82 | return configPath, nil 83 | } 84 | } 85 | 86 | // 2. 在搜索路径中查找配置文件 87 | for _, path := range opts.SearchPaths { 88 | configFile := path + "/" + opts.ConfigName 89 | if _, err := os.Stat(configFile); err == nil { 90 | fmt.Printf("使用搜索路径中的配置文件: %s\n", configFile) 91 | return configFile, nil 92 | } 93 | } 94 | 95 | // 3. 使用默认值(即使文件不存在也返回,让后续逻辑处理) 96 | defaultPath := opts.ConfigName 97 | fmt.Printf("使用默认配置文件路径: %s\n", defaultPath) 98 | return defaultPath, nil 99 | } 100 | 101 | // Reload 重新加载配置 102 | func Reload(v *viper.Viper, cfg interface{}) error { 103 | if err := v.ReadInConfig(); err != nil { 104 | return fmt.Errorf("重新读取配置文件失败: %w", err) 105 | } 106 | if err := v.Unmarshal(cfg); err != nil { 107 | return fmt.Errorf("重新解析配置失败: %w", err) 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /scripts/build-web.ps1: -------------------------------------------------------------------------------- 1 | # 前端构建脚本(Windows PowerShell 版本) 2 | # 作者: ruan 3 | # 说明: 构建前端项目并复制到 embed 目录 4 | 5 | $ErrorActionPreference = "Stop" 6 | 7 | # 获取项目根目录 8 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 9 | $ProjectRoot = Split-Path -Parent $ScriptDir 10 | 11 | Write-Host "📦 开始构建前端项目..." -ForegroundColor Green 12 | 13 | # 检查 Node.js 是否安装 14 | try { 15 | $nodeVersion = node --version 16 | Write-Host "✓ Node.js 版本: $nodeVersion" -ForegroundColor Green 17 | } catch { 18 | Write-Host "❌ 错误: 未找到 Node.js" -ForegroundColor Red 19 | Write-Host "请先安装 Node.js: https://nodejs.org/" -ForegroundColor Yellow 20 | exit 1 21 | } 22 | 23 | # 检查 pnpm 是否安装 24 | try { 25 | $pnpmVersion = pnpm --version 26 | Write-Host "✓ pnpm 版本: $pnpmVersion" -ForegroundColor Green 27 | } catch { 28 | Write-Host "❌ 错误: 未找到 pnpm" -ForegroundColor Red 29 | Write-Host "请先安装 pnpm: npm install -g pnpm 或 corepack enable" -ForegroundColor Yellow 30 | exit 1 31 | } 32 | 33 | # 进入 web 目录 34 | $WebDir = Join-Path $ProjectRoot "web" 35 | Set-Location $WebDir 36 | 37 | # 检查 package.json 是否存在 38 | if (-Not (Test-Path "package.json")) { 39 | Write-Host "❌ 错误: 未找到 package.json" -ForegroundColor Red 40 | exit 1 41 | } 42 | 43 | # 安装依赖(仅在 node_modules 不存在时) 44 | if (-Not (Test-Path "node_modules")) { 45 | Write-Host "📥 安装前端依赖..." -ForegroundColor Yellow 46 | pnpm install --frozen-lockfile 47 | if ($LASTEXITCODE -ne 0) { 48 | Write-Host "❌ 依赖安装失败" -ForegroundColor Red 49 | exit 1 50 | } 51 | } else { 52 | Write-Host "✓ 依赖已存在,跳过安装" -ForegroundColor Green 53 | } 54 | 55 | # 构建前端项目 56 | Write-Host "🔨 构建前端项目(生产模式)..." -ForegroundColor Yellow 57 | pnpm run build:prod 58 | if ($LASTEXITCODE -ne 0) { 59 | Write-Host "❌ 构建失败" -ForegroundColor Red 60 | exit 1 61 | } 62 | 63 | # 检查构建产物是否存在 64 | $DistDir = Join-Path $WebDir "dist" 65 | if (-Not (Test-Path $DistDir)) { 66 | Write-Host "❌ 错误: 构建失败,未找到 dist 目录" -ForegroundColor Red 67 | exit 1 68 | } 69 | 70 | # 返回项目根目录 71 | Set-Location $ProjectRoot 72 | 73 | # 目标目录 74 | $EmbedDir = Join-Path $ProjectRoot "internal\dashboard\public\dist" 75 | 76 | # 创建目标目录(如果不存在) 77 | if (-Not (Test-Path $EmbedDir)) { 78 | New-Item -ItemType Directory -Path $EmbedDir -Force | Out-Null 79 | } 80 | 81 | # 清空目标目录(保留 .gitkeep 或 README.md) 82 | Write-Host "🗑️ 清理 embed 目录..." -ForegroundColor Yellow 83 | Get-ChildItem -Path $EmbedDir -Recurse | 84 | Where-Object { $_.Name -ne '.gitkeep' -and $_.Name -ne 'README.md' } | 85 | Remove-Item -Recurse -Force 86 | 87 | # 复制构建产物 88 | Write-Host "📋 复制构建产物到 embed 目录..." -ForegroundColor Yellow 89 | Copy-Item -Path "$DistDir\*" -Destination $EmbedDir -Recurse -Force 90 | 91 | # 验证复制结果 92 | $AssetsDir = Join-Path $EmbedDir "assets" 93 | if (Test-Path $AssetsDir) { 94 | Write-Host "✅ 前端构建完成!" -ForegroundColor Green 95 | Write-Host " 输出目录: $EmbedDir" -ForegroundColor Green 96 | 97 | # 显示文件统计 98 | $FileCount = (Get-ChildItem -Path $EmbedDir -Recurse -File).Count 99 | Write-Host " 文件数量: $FileCount" -ForegroundColor Green 100 | } else { 101 | Write-Host "❌ 错误: 复制失败,未找到 assets 目录" -ForegroundColor Red 102 | exit 1 103 | } 104 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ============================================ 2 | # Simple Server Status Dashboard - 多阶段构建 3 | # 作者: ruan 4 | # 说明: 这个 Dockerfile 包含完整的前后端构建流程 5 | # ============================================ 6 | 7 | # ============================================ 8 | # 阶段 1: 前端构建 9 | # ============================================ 10 | FROM node:20-alpine AS frontend-builder 11 | 12 | WORKDIR /build/web 13 | 14 | # 启用 corepack 并安装 pnpm 15 | RUN corepack enable && corepack prepare pnpm@latest --activate 16 | 17 | # 复制前端依赖文件(利用 Docker 层缓存) 18 | COPY web/package.json web/pnpm-lock.yaml ./ 19 | 20 | # 安装依赖(包括构建工具) 21 | RUN pnpm install --frozen-lockfile 22 | 23 | # 复制前端源码 24 | COPY web/ ./ 25 | 26 | # 构建前端生产版本 27 | RUN pnpm run build:prod 28 | 29 | # ============================================ 30 | # 阶段 2: 后端构建 31 | # ============================================ 32 | FROM golang:1.23-alpine AS backend-builder 33 | 34 | # 安装构建依赖 35 | RUN apk add --no-cache git make 36 | 37 | WORKDIR /build 38 | 39 | # 复制 Go 依赖文件(利用 Docker 层缓存) 40 | COPY go.mod go.sum ./ 41 | RUN go mod download 42 | 43 | # 复制后端源码 44 | COPY cmd/ ./cmd/ 45 | COPY internal/ ./internal/ 46 | COPY pkg/ ./pkg/ 47 | 48 | # 从前端构建阶段复制构建产物 49 | COPY --from=frontend-builder /build/web/dist ./internal/dashboard/public/dist 50 | 51 | # 构建参数(可在 docker build 时传入) 52 | ARG VERSION=dev 53 | ARG COMMIT=unknown 54 | ARG BUILD_DATE=unknown 55 | 56 | # 编译后端(静态链接,无 CGO) 57 | RUN CGO_ENABLED=0 GOOS=linux go build \ 58 | -ldflags="-s -w \ 59 | -X main.version=${VERSION} \ 60 | -X main.commit=${COMMIT} \ 61 | -X main.date=${BUILD_DATE}" \ 62 | -trimpath \ 63 | -o /build/sss-dashboard \ 64 | ./cmd/dashboard 65 | 66 | # ============================================ 67 | # 阶段 3: 最终运行时镜像 68 | # ============================================ 69 | FROM alpine:latest 70 | 71 | # 构建参数 72 | ARG TZ="Asia/Shanghai" 73 | ENV TZ=${TZ} 74 | 75 | # 设置标签(OCI 标准) 76 | LABEL org.opencontainers.image.title="Simple Server Status Dashboard" 77 | LABEL org.opencontainers.image.description="极简服务器监控探针 - Dashboard" 78 | LABEL org.opencontainers.image.authors="ruan" 79 | LABEL org.opencontainers.image.url="https://github.com/ruanun/simple-server-status" 80 | LABEL org.opencontainers.image.source="https://github.com/ruanun/simple-server-status" 81 | LABEL org.opencontainers.image.licenses="MIT" 82 | 83 | # 安装运行时依赖 84 | RUN apk upgrade --no-cache && \ 85 | apk add --no-cache \ 86 | bash \ 87 | tzdata \ 88 | ca-certificates \ 89 | wget && \ 90 | ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \ 91 | echo ${TZ} > /etc/timezone && \ 92 | rm -rf /var/cache/apk/* 93 | 94 | WORKDIR /app 95 | 96 | # 从构建阶段复制二进制文件 97 | COPY --from=backend-builder /build/sss-dashboard ./sssd 98 | 99 | # 创建非 root 用户(安全性最佳实践) 100 | RUN addgroup -g 1000 sssd && \ 101 | adduser -D -u 1000 -G sssd sssd && \ 102 | chown -R sssd:sssd /app && \ 103 | chmod +x /app/sssd 104 | 105 | # 切换到非 root 用户 106 | USER sssd 107 | 108 | # 健康检查(每 30 秒检查一次) 109 | HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ 110 | CMD wget --quiet --tries=1 --spider http://localhost:8900/api/statistics || exit 1 111 | 112 | # 环境变量 113 | ENV CONFIG="sss-dashboard.yaml" 114 | 115 | # 暴露端口 116 | EXPOSE 8900 117 | 118 | # 启动命令 119 | CMD ["/app/sssd"] 120 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: '发布版本号(例如:v1.2.3)' 11 | required: true 12 | type: string 13 | skip_docker: 14 | description: '跳过 Docker 镜像构建' 15 | required: false 16 | type: boolean 17 | default: false 18 | 19 | permissions: 20 | contents: write 21 | packages: write 22 | 23 | jobs: 24 | release: 25 | name: 发布新版本 26 | runs-on: ubuntu-latest 27 | env: 28 | VERSION: ${{ github.event.inputs.version || github.ref_name }} 29 | steps: 30 | - name: 检出代码 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | ref: ${{ github.ref }} 35 | 36 | - name: 设置 Go 环境 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: '1.23.2' 40 | cache: true 41 | 42 | - name: 安装 pnpm 43 | uses: pnpm/action-setup@v2 44 | with: 45 | version: 10 46 | 47 | - name: 设置 Node.js 环境 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: '20' 51 | cache: 'pnpm' 52 | cache-dependency-path: web/pnpm-lock.yaml 53 | 54 | - name: 构建前端 55 | run: bash scripts/build-web.sh 56 | 57 | - name: 运行 GoReleaser 58 | uses: goreleaser/goreleaser-action@v6 59 | with: 60 | distribution: goreleaser 61 | version: '~> v2' 62 | args: release --clean 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | docker: 67 | name: 构建 Docker 镜像 68 | runs-on: ubuntu-latest 69 | needs: release 70 | if: ${{ github.event.inputs.skip_docker != 'true' }} 71 | env: 72 | VERSION: ${{ github.event.inputs.version || github.ref_name }} 73 | steps: 74 | - name: 检出代码 75 | uses: actions/checkout@v4 76 | 77 | - name: 设置 Docker Buildx 78 | uses: docker/setup-buildx-action@v3 79 | 80 | - name: 登录 Docker Hub 81 | uses: docker/login-action@v3 82 | with: 83 | username: ${{ secrets.DOCKER_USERNAME }} 84 | password: ${{ secrets.DOCKER_PASSWORD }} 85 | 86 | - name: 提取版本信息 87 | id: meta 88 | uses: docker/metadata-action@v5 89 | with: 90 | images: | 91 | ruanun/sssd 92 | tags: | 93 | type=semver,pattern={{version}} 94 | type=semver,pattern={{major}}.{{minor}} 95 | type=semver,pattern={{major}} 96 | type=raw,value=latest,enable={{is_default_branch}} 97 | 98 | - name: 构建并推送 Docker 镜像 99 | uses: docker/build-push-action@v5 100 | with: 101 | context: . 102 | file: ./Dockerfile # 使用新的多阶段构建 Dockerfile(自包含前后端构建) 103 | platforms: linux/amd64,linux/arm64,linux/arm/v7 104 | push: true 105 | tags: ${{ steps.meta.outputs.tags }} 106 | labels: ${{ steps.meta.outputs.labels }} 107 | build-args: | 108 | VERSION=${{ env.VERSION }} 109 | COMMIT=${{ github.sha }} 110 | BUILD_DATE=${{ github.event.repository.updated_at }} 111 | TZ=Asia/Shanghai 112 | cache-from: type=gha 113 | cache-to: type=gha,mode=max 114 | -------------------------------------------------------------------------------- /web/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import type {AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse} from "axios"; 3 | import {message} from "ant-design-vue"; 4 | import {checkStatus} from "@/api/helper/checkStatus"; 5 | import { ResponseCode, type ApiError } from '@/types/api' 6 | 7 | 8 | // * 请求响应参数(不包含data) - 保持向后兼容 9 | export interface Result { 10 | code: number; 11 | message: string; 12 | } 13 | 14 | // * 请求响应参数(包含data) - 保持向后兼容 15 | export interface ResultData extends Result { 16 | data: T; 17 | } 18 | 19 | // 导出新的类型供外部使用 20 | export { ResponseCode, type ApiResponse, type ApiError } from '@/types/api' 21 | 22 | const config = { 23 | // 默认地址请求地址,可在 .env.*** 文件中修改 24 | baseURL: import.meta.env.VITE_BASE_URL as string, 25 | // 设置超时时间(30s) 26 | timeout: 30000, 27 | // 跨域时候允许携带凭证 28 | withCredentials: true 29 | }; 30 | 31 | // console.log("import.meta.env ", import.meta.env.MODE) 32 | 33 | class RequestHttp { 34 | service: AxiosInstance; 35 | 36 | public constructor(config: AxiosRequestConfig) { 37 | // 实例化axios 38 | this.service = axios.create(config); 39 | 40 | /** 41 | * @description 响应拦截器 42 | * 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息 43 | */ 44 | this.service.interceptors.response.use( 45 | (response: AxiosResponse) => { 46 | const {data} = response; 47 | if (data.code && data.code !== ResponseCode.SUCCESS) { 48 | message.error(data.message); 49 | return Promise.reject(data); 50 | } 51 | return data; 52 | }, 53 | async (error: AxiosError) => { 54 | const {response} = error; 55 | // 请求超时 && 网络错误单独判断,没有 response 56 | if (error.message.indexOf("timeout") !== -1) message.error("请求超时!请您稍后重试"); 57 | if (error.message.indexOf("Network Error") !== -1) message.error("网络错误!请您稍后重试"); 58 | // 根据响应的错误状态码,做不同的处理 59 | if (response) checkStatus(response.status); 60 | // 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面 61 | // if (!window.navigator.onLine) router.replace("/500"); 62 | 63 | // 返回标准化的错误对象 64 | const apiError: ApiError = { 65 | code: response?.status || ResponseCode.INTERNAL_ERROR, 66 | message: error.message || '未知错误', 67 | originalError: error 68 | } 69 | return Promise.reject(apiError); 70 | } 71 | ); 72 | } 73 | 74 | // * 常用请求方法封装 75 | get(url: string, params?: object, _object = {}): Promise> { 76 | return this.service.get(url, {params, ..._object}); 77 | } 78 | 79 | post(url: string, params?: object, _object = {}): Promise> { 80 | return this.service.post(url, params, _object); 81 | } 82 | 83 | put(url: string, params?: object, _object = {}): Promise> { 84 | return this.service.put(url, params, _object); 85 | } 86 | 87 | delete(url: string, params?: any, _object = {}): Promise> { 88 | return this.service.delete(url, {params, ..._object}); 89 | } 90 | 91 | download(url: string, params?: object, _object = {}): Promise { 92 | return this.service.post(url, params, {..._object, responseType: "blob"}); 93 | } 94 | } 95 | 96 | export default new RequestHttp(config); 97 | -------------------------------------------------------------------------------- /cmd/dashboard/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "go.uber.org/zap" 8 | 9 | internal "github.com/ruanun/simple-server-status/internal/dashboard" 10 | "github.com/ruanun/simple-server-status/internal/dashboard/config" 11 | "github.com/ruanun/simple-server-status/internal/dashboard/global" 12 | "github.com/ruanun/simple-server-status/internal/dashboard/server" 13 | "github.com/ruanun/simple-server-status/internal/shared/app" 14 | ) 15 | 16 | func main() { 17 | // 创建应用 18 | application := app.New("SSS-Dashboard", app.BuildInfo{ 19 | GitCommit: global.GitCommit, 20 | Version: global.Version, 21 | BuiltAt: global.BuiltAt, 22 | GoVersion: global.GoVersion, 23 | }) 24 | 25 | // 打印构建信息 26 | application.PrintBuildInfo() 27 | 28 | // 运行应用 29 | if err := run(application); err != nil { 30 | panic(fmt.Errorf("应用启动失败: %v", err)) 31 | } 32 | 33 | // 等待关闭信号 34 | application.WaitForShutdown() 35 | } 36 | 37 | func run(application *app.Application) error { 38 | // 使用指针捕获,以便热加载回调能访问到 dashboardService 39 | var dashboardService *internal.DashboardService 40 | 41 | // 1. 加载配置(支持热加载) 42 | cfg, err := loadConfig(&dashboardService) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | // 2. 初始化日志 48 | logger, err := initLogger(cfg) 49 | if err != nil { 50 | return err 51 | } 52 | application.SetLogger(logger) 53 | application.RegisterCleanup(func() error { 54 | return logger.Sync() 55 | }) 56 | 57 | // 3. 创建错误处理器 58 | errorHandler := internal.NewErrorHandler(logger) 59 | 60 | // 4. 初始化 HTTP 服务器(Gin 引擎) 61 | ginEngine := server.InitServer(cfg, logger, errorHandler) 62 | 63 | // 5. 创建并启动 Dashboard 服务 64 | dashboardService, err = internal.NewDashboardService(cfg, logger, ginEngine, errorHandler) 65 | if err != nil { 66 | return fmt.Errorf("创建 Dashboard 服务失败: %w", err) 67 | } 68 | 69 | // 启动服务 70 | if err := dashboardService.Start(); err != nil { 71 | return fmt.Errorf("启动 Dashboard 服务失败: %w", err) 72 | } 73 | 74 | // 6. 注册清理函数 75 | registerCleanups(application, dashboardService) 76 | 77 | return nil 78 | } 79 | 80 | func loadConfig(dashboardServicePtr **internal.DashboardService) (*config.DashboardConfig, error) { 81 | // 使用闭包捕获配置指针以支持热加载 82 | var currentCfg *config.DashboardConfig 83 | 84 | cfg, err := app.LoadConfig[*config.DashboardConfig]( 85 | "sss-dashboard.yaml", 86 | "yaml", 87 | []string{".", "./configs", "/etc/sssa", "/etc/sss"}, 88 | true, 89 | func(newCfg *config.DashboardConfig) error { 90 | // 热加载回调:更新已返回配置对象的内容 91 | if currentCfg != nil { 92 | // 验证和设置默认值 93 | if err := internal.ValidateAndApplyDefaults(newCfg); err != nil { 94 | return fmt.Errorf("热加载配置验证失败: %w", err) 95 | } 96 | 97 | // 同步更新 servers map(如果 dashboardService 已创建) 98 | if dashboardServicePtr != nil && *dashboardServicePtr != nil { 99 | (*dashboardServicePtr).ReloadServers(newCfg.Servers) 100 | } 101 | 102 | *currentCfg = *newCfg 103 | fmt.Println("[INFO] Dashboard 配置已热加载") 104 | } 105 | return nil 106 | }, 107 | ) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | // 保存配置指针供闭包使用 113 | currentCfg = cfg 114 | 115 | // 详细验证和设置默认值 116 | if err := internal.ValidateAndApplyDefaults(cfg); err != nil { 117 | return nil, fmt.Errorf("配置验证失败: %w", err) 118 | } 119 | 120 | return cfg, nil 121 | } 122 | 123 | func initLogger(cfg *config.DashboardConfig) (*zap.SugaredLogger, error) { 124 | return app.InitLogger(cfg.LogLevel, cfg.LogPath) 125 | } 126 | 127 | func registerCleanups(application *app.Application, dashboardService *internal.DashboardService) { 128 | // 注册 Dashboard 服务清理 129 | // 设置 10 秒超时用于优雅关闭 130 | application.RegisterCleanup(func() error { 131 | return dashboardService.Stop(10 * time.Second) 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /web/src/components/StatusIndicator.vue: -------------------------------------------------------------------------------- 1 | 21 | 33 | 34 | 77 | 78 | 176 | -------------------------------------------------------------------------------- /internal/shared/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "go.uber.org/zap" 13 | ) 14 | 15 | // Application 应用程序生命周期管理器 16 | type Application struct { 17 | name string 18 | version BuildInfo 19 | logger *zap.SugaredLogger 20 | ctx context.Context 21 | cancel context.CancelFunc 22 | cleanup []CleanupFunc 23 | } 24 | 25 | // BuildInfo 构建信息 26 | type BuildInfo struct { 27 | GitCommit string 28 | Version string 29 | BuiltAt string 30 | GoVersion string 31 | } 32 | 33 | // CleanupFunc 清理函数类型 34 | type CleanupFunc func() error 35 | 36 | // New 创建应用实例 37 | func New(name string, version BuildInfo) *Application { 38 | ctx, cancel := context.WithCancel(context.Background()) 39 | return &Application{ 40 | name: name, 41 | version: version, 42 | ctx: ctx, 43 | cancel: cancel, 44 | cleanup: make([]CleanupFunc, 0), 45 | } 46 | } 47 | 48 | // SetLogger 设置日志器 49 | func (a *Application) SetLogger(logger *zap.SugaredLogger) { 50 | a.logger = logger 51 | } 52 | 53 | // RegisterCleanup 注册清理函数 54 | func (a *Application) RegisterCleanup(fn CleanupFunc) { 55 | a.cleanup = append(a.cleanup, fn) 56 | } 57 | 58 | // PrintBuildInfo 打印构建信息 59 | func (a *Application) PrintBuildInfo() { 60 | fmt.Printf("=== %s ===\n", a.name) 61 | fmt.Printf("版本: %s\n", a.version.Version) 62 | fmt.Printf("Git提交: %s\n", a.version.GitCommit) 63 | fmt.Printf("构建时间: %s\n", a.version.BuiltAt) 64 | fmt.Printf("Go版本: %s\n", a.version.GoVersion) 65 | fmt.Println("================") 66 | } 67 | 68 | // WaitForShutdown 等待关闭信号 69 | func (a *Application) WaitForShutdown() { 70 | signalChan := make(chan os.Signal, 1) 71 | signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) 72 | 73 | sig := <-signalChan 74 | if a.logger != nil { 75 | a.logger.Infof("接收到信号: %s,开始优雅关闭", sig) 76 | } else { 77 | fmt.Printf("接收到信号: %s,开始优雅关闭\n", sig) 78 | } 79 | 80 | a.Shutdown() 81 | } 82 | 83 | // Shutdown 执行清理 84 | // 按照 LIFO 顺序执行所有清理函数,每个函数都有超时保护 85 | func (a *Application) Shutdown() { 86 | a.cancel() 87 | 88 | // 收集所有清理错误 89 | var cleanupErrors []string 90 | cleanupTimeout := 15 * time.Second // 每个清理函数的超时时间 91 | 92 | // 按相反顺序执行清理函数 93 | for i := len(a.cleanup) - 1; i >= 0; i-- { 94 | // 为每个清理函数创建带超时的上下文 95 | ctx, cancel := context.WithTimeout(context.Background(), cleanupTimeout) 96 | 97 | // 在 channel 中执行清理函数,以便支持超时控制 98 | done := make(chan error, 1) 99 | go func(fn CleanupFunc) { 100 | done <- fn() 101 | }(a.cleanup[i]) 102 | 103 | // 等待完成或超时 104 | select { 105 | case err := <-done: 106 | cancel() // 清理完成,取消超时上下文 107 | if err != nil { 108 | errMsg := fmt.Sprintf("清理函数 #%d 执行失败: %v", len(a.cleanup)-i, err) 109 | cleanupErrors = append(cleanupErrors, errMsg) 110 | if a.logger != nil { 111 | a.logger.Errorf(errMsg) 112 | } else { 113 | fmt.Printf("%s\n", errMsg) 114 | } 115 | } else { 116 | if a.logger != nil { 117 | a.logger.Debugf("清理函数 #%d 执行成功", len(a.cleanup)-i) 118 | } 119 | } 120 | case <-ctx.Done(): 121 | cancel() // 超时,取消上下文 122 | errMsg := fmt.Sprintf("清理函数 #%d 执行超时(超过 %v)", len(a.cleanup)-i, cleanupTimeout) 123 | cleanupErrors = append(cleanupErrors, errMsg) 124 | if a.logger != nil { 125 | a.logger.Warnf(errMsg) 126 | } else { 127 | fmt.Printf("%s\n", errMsg) 128 | } 129 | } 130 | } 131 | 132 | // 汇总清理结果 133 | if len(cleanupErrors) > 0 { 134 | summary := fmt.Sprintf("应用关闭完成,但有 %d 个清理函数失败:\n%s", 135 | len(cleanupErrors), 136 | strings.Join(cleanupErrors, "\n")) 137 | if a.logger != nil { 138 | a.logger.Warn(summary) 139 | } else { 140 | fmt.Println(summary) 141 | } 142 | } else { 143 | if a.logger != nil { 144 | a.logger.Info("应用已优雅关闭,所有清理函数执行成功") 145 | } else { 146 | fmt.Println("应用已优雅关闭,所有清理函数执行成功") 147 | } 148 | } 149 | } 150 | 151 | // Context 获取应用上下文 152 | func (a *Application) Context() context.Context { 153 | return a.ctx 154 | } 155 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | security-events: write 6 | 7 | on: 8 | push: 9 | branches: [ master, main, develop ] 10 | pull_request: 11 | branches: [ master, main, develop ] 12 | 13 | jobs: 14 | lint: 15 | name: 代码检查 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: 检出代码 19 | uses: actions/checkout@v4 20 | 21 | - name: 安装 pnpm 22 | uses: pnpm/action-setup@v2 23 | with: 24 | version: 10 25 | 26 | - name: 设置 Node.js 环境 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: '20' 30 | cache: 'pnpm' 31 | cache-dependency-path: web/pnpm-lock.yaml 32 | 33 | - name: 构建前端 34 | run: bash scripts/build-web.sh 35 | 36 | - name: 设置 Go 环境 37 | uses: actions/setup-go@v5 38 | with: 39 | go-version: '1.23.2' 40 | cache: true 41 | 42 | - name: 安装 golangci-lint 43 | run: | 44 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.6.2 45 | 46 | - name: 运行 golangci-lint 47 | run: golangci-lint run --timeout=5m ./... 48 | 49 | - name: 运行 go vet 50 | run: go vet ./... 51 | 52 | build: 53 | name: 构建测试 54 | runs-on: ${{ matrix.os }} 55 | strategy: 56 | matrix: 57 | os: [ubuntu-latest, windows-latest, macos-latest] 58 | go-version: ['1.23.2'] 59 | steps: 60 | - name: 检出代码 61 | uses: actions/checkout@v4 62 | 63 | - name: 设置 Go 环境 64 | uses: actions/setup-go@v5 65 | with: 66 | go-version: ${{ matrix.go-version }} 67 | cache: true 68 | 69 | - name: 安装 pnpm 70 | uses: pnpm/action-setup@v2 71 | with: 72 | version: 10 73 | 74 | - name: 设置 Node.js 环境 75 | uses: actions/setup-node@v4 76 | with: 77 | node-version: '20' 78 | cache: 'pnpm' 79 | cache-dependency-path: web/pnpm-lock.yaml 80 | 81 | - name: 构建前端(Unix 系统) 82 | if: matrix.os != 'windows-latest' 83 | run: bash scripts/build-web.sh 84 | 85 | - name: 构建前端(Windows 系统) 86 | if: matrix.os == 'windows-latest' 87 | shell: pwsh 88 | run: | 89 | $PSDefaultParameterValues['*:Encoding'] = 'utf8' 90 | & "scripts/build-web.ps1" 91 | 92 | - name: 构建 Agent(Windows) 93 | if: matrix.os == 'windows-latest' 94 | run: go build -v -o bin/sss-agent.exe ./cmd/agent 95 | 96 | - name: 构建 Agent(非 Windows) 97 | if: matrix.os != 'windows-latest' 98 | run: go build -v -o bin/sss-agent ./cmd/agent 99 | 100 | - name: 构建 Dashboard(Windows) 101 | if: matrix.os == 'windows-latest' 102 | run: go build -v -o bin/sss-dashboard.exe ./cmd/dashboard 103 | 104 | - name: 构建 Dashboard(非 Windows) 105 | if: matrix.os != 'windows-latest' 106 | run: go build -v -o bin/sss-dashboard ./cmd/dashboard 107 | 108 | - name: 验证二进制文件 109 | if: matrix.os != 'windows-latest' 110 | run: | 111 | chmod +x bin/sss-agent 112 | chmod +x bin/sss-dashboard 113 | file bin/sss-agent 114 | file bin/sss-dashboard 115 | 116 | security: 117 | name: 安全检查 118 | runs-on: ubuntu-latest 119 | steps: 120 | - name: 检出代码 121 | uses: actions/checkout@v4 122 | 123 | - name: 设置 Go 环境 124 | uses: actions/setup-go@v5 125 | with: 126 | go-version: '1.23.2' 127 | cache: true 128 | 129 | - name: 运行 Gosec 安全扫描 130 | uses: securego/gosec@master 131 | with: 132 | args: '-fmt sarif -out results.sarif ./...' 133 | continue-on-error: true 134 | 135 | - name: 上传 SARIF 文件 136 | uses: github/codeql-action/upload-sarif@v4 137 | with: 138 | sarif_file: results.sarif 139 | if: always() 140 | -------------------------------------------------------------------------------- /web/src/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据格式化工具函数 3 | * 用于格式化服务器监控数据的显示 4 | * @author ruan 5 | */ 6 | 7 | import { getLocale } from '@/locales' 8 | 9 | /** 10 | * 格式化运行时间 11 | * @param uptimeSeconds 运行时间(秒) 12 | * @returns 格式化后的字符串 13 | * @example 14 | * formatUptime(1562560) // 中文: "18天 6小时 32分" 英文: "18d 6h 32m" 15 | */ 16 | export function formatUptime(uptimeSeconds: number): string { 17 | if (uptimeSeconds === undefined || uptimeSeconds === null || uptimeSeconds === 0) { 18 | const locale = getLocale() 19 | return locale === 'zh-CN' ? '0天' : '0d' 20 | } 21 | 22 | const days = Math.floor(uptimeSeconds / (60 * 60 * 24)) 23 | const hours = Math.floor((uptimeSeconds % (60 * 60 * 24)) / (60 * 60)) 24 | const minutes = Math.floor((uptimeSeconds % (60 * 60)) / 60) 25 | 26 | const locale = getLocale() 27 | 28 | if (locale === 'zh-CN') { 29 | // 中文格式:18天 6小时 32分 30 | const parts: string[] = [] 31 | if (days > 0) parts.push(`${days}天`) 32 | if (hours > 0) parts.push(`${hours}小时`) 33 | if (minutes > 0) parts.push(`${minutes}分`) 34 | if (parts.length === 0) parts.push('0天') 35 | return parts.join(' ') 36 | } else { 37 | // 英文格式:18d 6h 32m 38 | const parts: string[] = [] 39 | if (days > 0) parts.push(`${days}d`) 40 | if (hours > 0) parts.push(`${hours}h`) 41 | if (minutes > 0) parts.push(`${minutes}m`) 42 | if (parts.length === 0) parts.push('0d') 43 | return parts.join(' ') 44 | } 45 | } 46 | 47 | /** 48 | * 格式化系统负载 49 | * @param load1 1分钟负载 50 | * @param load5 5分钟负载 51 | * @param load15 15分钟负载 52 | * @returns 格式化后的字符串 "1.2 / 1.5 / 1.9" 53 | */ 54 | export function formatLoad(load1: number, load5: number, load15: number): string { 55 | return `${load1.toFixed(1)} / ${load5.toFixed(1)} / ${load15.toFixed(1)}` 56 | } 57 | 58 | /** 59 | * 格式化百分比 60 | * @param percent 百分比数值 61 | * @returns 格式化后的整数百分比 62 | */ 63 | export function formatPercent(percent: number): number { 64 | return Math.round(percent) 65 | } 66 | 67 | /** 68 | * 格式化内存显示(带单位和百分比) 69 | * @param used 已用内存(字节) 70 | * @param total 总内存(字节) 71 | * @param readableBytes 字节转换函数 72 | * @returns 格式化后的字符串 "3.1GB / 5.0GB" 73 | */ 74 | export function formatMemory( 75 | used: number, 76 | total: number, 77 | readableBytes: (bytes: number) => string 78 | ): string { 79 | return `${readableBytes(used)} / ${readableBytes(total)}` 80 | } 81 | 82 | /** 83 | * 格式化时间间隔 84 | * 计算从指定时间到现在的时间差,并格式化为易读字符串 85 | * 86 | * @param startTime 开始时间 87 | * @param locale 语言环境(可选,默认从全局配置获取) 88 | * @returns 格式化后的字符串 89 | * 90 | * @example 91 | * ```typescript 92 | * const startTime = new Date('2024-01-01 10:00:00') 93 | * formatTimeInterval(startTime, 'zh-CN') // "2天 5小时" 或 "5小时 30分钟" 等 94 | * formatTimeInterval(startTime, 'en-US') // "2d 5h" 或 "5h 30m" 等 95 | * ``` 96 | */ 97 | export function formatTimeInterval(startTime: Date | null, locale?: string): string { 98 | if (!startTime) return '-' 99 | 100 | const currentLocale = locale || getLocale() 101 | const now = new Date() 102 | const diffMs = now.getTime() - startTime.getTime() 103 | const diffSeconds = Math.floor(diffMs / 1000) 104 | const diffMinutes = Math.floor(diffSeconds / 60) 105 | const diffHours = Math.floor(diffMinutes / 60) 106 | const diffDays = Math.floor(diffHours / 24) 107 | 108 | if (currentLocale === 'zh-CN') { 109 | if (diffDays > 0) { 110 | const hours = diffHours % 24 111 | return `${diffDays}天 ${hours}小时` 112 | } else if (diffHours > 0) { 113 | const minutes = diffMinutes % 60 114 | return `${diffHours}小时 ${minutes}分钟` 115 | } else if (diffMinutes > 0) { 116 | return `${diffMinutes}分钟` 117 | } else { 118 | return `${diffSeconds}秒` 119 | } 120 | } else { 121 | if (diffDays > 0) { 122 | const hours = diffHours % 24 123 | return `${diffDays}d ${hours}h` 124 | } else if (diffHours > 0) { 125 | const minutes = diffMinutes % 60 126 | return `${diffHours}h ${minutes}m` 127 | } else if (diffMinutes > 0) { 128 | return `${diffMinutes}m` 129 | } else { 130 | return `${diffSeconds}s` 131 | } 132 | } 133 | } 134 | 135 | -------------------------------------------------------------------------------- /pkg/model/RespServerInfo.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/shirou/gopsutil/v4/load" 7 | ) 8 | 9 | type RespServerInfo struct { 10 | Name string `json:"name"` //name展示 11 | Group string `json:"group"` //组 12 | Id string `json:"id"` //服务器id 13 | LastReportTime int64 `json:"lastReportTime"` //最后上报时间 14 | 15 | Uptime uint64 `json:"uptime"` //服务器的uptime //单位秒 16 | Platform string `json:"platform"` //系统版型信息 ex: Windows 11 x64 ;platform+platformVersion 17 | 18 | CpuPercent float64 `json:"cpuPercent"` //cpu占用 19 | RAMPercent float64 `json:"RAMPercent"` //内存占用 20 | SWAPPercent float64 `json:"SWAPPercent"` //swap占用 21 | DiskPercent float64 `json:"diskPercent"` //硬盘占用 22 | 23 | NetInSpeed uint64 `json:"netInSpeed"` //下载速度 24 | NetOutSpeed uint64 `json:"netOutSpeed"` //上传速度 25 | 26 | Loc string `json:"loc"` 27 | 28 | HostInfo *RespHostData `json:"hostInfo"` 29 | 30 | IsOnline bool `json:"isOnline"` //是否在线 31 | } 32 | 33 | type RespHostData struct { 34 | CpuInfo []string `json:"cpuInfo"` //cpu信息字符串描述 35 | AvgStat *load.AvgStat `json:"avgStat"` //load 36 | 37 | RAMTotal uint64 `json:"RAMTotal"` 38 | RAMUsed uint64 `json:"RAMUsed"` 39 | SwapTotal uint64 `json:"swapTotal"` 40 | SwapUsed uint64 `json:"swapUsed"` 41 | 42 | DiskTotal uint64 `json:"diskTotal"` //总硬盘 43 | DiskUsed uint64 `json:"diskUsed"` //已使用 44 | DiskPartitions []*Partition `json:"diskPartitions"` //各个分区 45 | 46 | NetInTransfer uint64 `json:"netInTransfer"` //下载的流量 47 | NetOutTransfer uint64 `json:"netOutTransfer"` //上传的流量 48 | 49 | OS string `json:"os"` 50 | Platform string `json:"platform"` 51 | PlatformVersion string `json:"platformVersion"` 52 | VirtualizationSystem string `json:"virtualizationSystem"` 53 | KernelVersion string `json:"kernelVersion"` 54 | KernelArch string `json:"kernelArch"` 55 | } 56 | 57 | func isWin(serverInfo *ServerInfo) bool { 58 | return strings.Contains(serverInfo.HostInfo.Platform, "Windows") 59 | } 60 | 61 | func NewRespHostData(serverInfo *ServerInfo) *RespHostData { 62 | return &RespHostData{ 63 | CpuInfo: serverInfo.CpuInfo.Info, 64 | AvgStat: serverInfo.HostInfo.AvgStat, 65 | 66 | RAMTotal: serverInfo.VirtualMemoryInfo.Total, 67 | RAMUsed: serverInfo.VirtualMemoryInfo.Used, 68 | SwapTotal: serverInfo.SwapMemoryInfo.Total, 69 | SwapUsed: serverInfo.SwapMemoryInfo.Used, 70 | 71 | DiskTotal: serverInfo.DiskInfo.Total, 72 | DiskUsed: serverInfo.DiskInfo.Used, 73 | DiskPartitions: serverInfo.DiskInfo.Partitions, 74 | 75 | NetInTransfer: serverInfo.NetworkInfo.NetInTransfer, 76 | NetOutTransfer: serverInfo.NetworkInfo.NetOutTransfer, 77 | 78 | OS: serverInfo.HostInfo.OS, 79 | Platform: serverInfo.HostInfo.Platform, 80 | PlatformVersion: serverInfo.HostInfo.PlatformVersion, 81 | VirtualizationSystem: serverInfo.HostInfo.VirtualizationSystem, 82 | KernelVersion: serverInfo.HostInfo.KernelVersion, 83 | KernelArch: serverInfo.HostInfo.KernelArch, 84 | } 85 | } 86 | 87 | func NewRespServerInfo(serverInfo *ServerInfo) *RespServerInfo { 88 | var platform string 89 | if isWin(serverInfo) { 90 | platform = serverInfo.HostInfo.Platform 91 | } else { 92 | platform = serverInfo.HostInfo.Platform + " " + serverInfo.HostInfo.PlatformVersion 93 | } 94 | return &RespServerInfo{ 95 | Name: serverInfo.Name, 96 | Group: serverInfo.Group, 97 | Id: serverInfo.Id, 98 | LastReportTime: serverInfo.LastReportTime, 99 | 100 | Uptime: serverInfo.HostInfo.Uptime, 101 | Platform: platform, 102 | 103 | CpuPercent: serverInfo.CpuInfo.Percent, 104 | RAMPercent: serverInfo.VirtualMemoryInfo.UsedPercent, 105 | SWAPPercent: serverInfo.SwapMemoryInfo.UsedPercent, 106 | DiskPercent: serverInfo.DiskInfo.UsedPercent, 107 | NetInSpeed: serverInfo.NetworkInfo.NetInSpeed, 108 | NetOutSpeed: serverInfo.NetworkInfo.NetOutSpeed, 109 | 110 | Loc: serverInfo.Loc, 111 | 112 | //其他信息 113 | HostInfo: NewRespHostData(serverInfo), 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | run: 4 | timeout: 5m 5 | tests: true 6 | build-tags: 7 | - integration 8 | 9 | linters: 10 | # 禁用默认的 linters,只启用我们指定的 11 | disable-all: true 12 | 13 | enable: 14 | # 核心检查(保留) 15 | - govet # Go 官方审查工具 16 | - ineffassign # 检测无效赋值 17 | - staticcheck # 静态分析(最全面的检查,已包含大部分错误检查) 18 | - unused # 检测未使用的代码 19 | - gosec # 安全检查 20 | - misspell # 拼写错误检查 21 | 22 | # 明确禁用噪音过多的 linters 23 | disable: 24 | - errcheck # 未检查错误(噪音太多) 25 | 26 | # 已禁用(减少噪音): 27 | # - errcheck # 错误检查(噪音太多,staticcheck 已覆盖关键部分) 28 | # - gocyclo # 复杂度检查(过于严格) 29 | # - dupl # 代码重复(误报多) 30 | # - gocritic # 代码评审(规则太多) 31 | # - revive # 风格检查(与 staticcheck 重复) 32 | # - unconvert # 类型转换(小问题) 33 | # - unparam # 未使用参数(噪音多) 34 | # - predeclared # 遮蔽检查(影响小) 35 | 36 | formatters: 37 | enable: 38 | - gofmt # 格式检查 39 | - goimports # import 排序 40 | settings: 41 | gofmt: 42 | simplify: true # 使用 -s 简化代码 43 | goimports: 44 | local-prefixes: [] # 本地包前缀(可选配置) 45 | 46 | linters-settings: 47 | errcheck: 48 | check-type-assertions: false # 不检查类型断言 49 | check-blank: false # 不检查空白标识符 50 | # 排除常见的可以忽略错误的函数 51 | exclude-functions: 52 | # 数据库相关 53 | - (*database/sql.DB).Close 54 | - (*database/sql.Rows).Close 55 | # 文件和IO 56 | - (*os.File).Close 57 | - (io.Closer).Close 58 | - (*net/http.Response).Body.Close 59 | # WebSocket 60 | - (*github.com/gorilla/websocket.Conn).Close 61 | - (*github.com/olahol/melody.Session).Close 62 | - (*github.com/olahol/melody.Session).Write 63 | - (*github.com/olahol/melody.Session).CloseWithMsg 64 | - (*github.com/olahol/melody.Melody).Broadcast 65 | - (*github.com/olahol/melody.Melody).Close 66 | - (*github.com/olahol/melody.Melody).HandleRequest 67 | # HTTP 68 | - (net/http.ResponseWriter).Write 69 | # 格式化输出 70 | - fmt.Fprintf 71 | - fmt.Fprintln 72 | # 验证器 73 | - (*github.com/go-playground/validator/v10.Validate).RegisterValidation 74 | # 其他 75 | - (*.AgentUpdate).Update 76 | - (*.NetworkStatsCollector).Update 77 | 78 | govet: 79 | enable-all: false 80 | # 只启用最重要的检查 81 | enable: 82 | - assign 83 | - atomic 84 | - bools 85 | - buildtag 86 | - nilfunc 87 | - printf 88 | 89 | misspell: 90 | locale: US 91 | 92 | staticcheck: 93 | # 排除一些检查以减少噪音 94 | checks: 95 | - "all" 96 | - "-ST1000" # 包注释 97 | - "-ST1003" # 首字母缩写 98 | - "-SA5011" # possible nil pointer dereference (误报多) 99 | # 注意:staticcheck 不包含未检查错误的检查,那些是 errcheck 的 100 | 101 | gosec: 102 | # 排除一些低风险的检查 103 | excludes: 104 | - G104 # 未检查错误(由 errcheck 处理) 105 | - G304 # 文件路径由用户输入(某些场景下合理) 106 | 107 | issues: 108 | exclude-rules: 109 | # 排除测试文件的某些检查 110 | - path: _test\.go 111 | linters: 112 | - errcheck # 测试中可以不检查某些错误 113 | - gosec # 测试中的安全检查不那么严格 114 | 115 | # 排除生成的文件 116 | - path: \.pb\.go$ 117 | linters: 118 | - all 119 | 120 | # 排除 vendor 目录 121 | - path: vendor/ 122 | linters: 123 | - all 124 | 125 | # 排除 defer 中的 Close 调用 126 | - text: "Error return value.*Close.*is not checked" 127 | linters: 128 | - errcheck 129 | 130 | # 排除非关键的环境设置 131 | - text: "Error return value of `os\\.(Setenv|RemoveAll)`" 132 | linters: 133 | - errcheck 134 | 135 | # 排除验证器注册错误(通常在初始化时处理) 136 | - text: "Error return value of.*RegisterValidation.*is not checked" 137 | linters: 138 | - errcheck 139 | 140 | # 排除 WebSocket 相关的非关键错误 141 | - text: "Error return value of.*\\.(HandleRequest|Broadcast|Write|CloseWithMsg)" 142 | linters: 143 | - errcheck 144 | 145 | # 限制每个 linter 的问题数量(避免输出过多) 146 | max-issues-per-linter: 50 147 | max-same-issues: 3 148 | 149 | output: 150 | formats: 151 | colored-line-number: 152 | path: stdout 153 | print-issued-lines: true 154 | print-linter-name: true 155 | -------------------------------------------------------------------------------- /scripts/build-docker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ============================================ 4 | # Docker 本地构建测试脚本 5 | # 作者: ruan 6 | # 用途: 在本地测试 Docker 镜像构建 7 | # ============================================ 8 | 9 | set -e 10 | 11 | # 颜色输出 12 | RED='\033[0;31m' 13 | GREEN='\033[0;32m' 14 | YELLOW='\033[1;33m' 15 | NC='\033[0m' # No Color 16 | 17 | # 配置变量 18 | IMAGE_NAME="sssd" 19 | TAG="dev" 20 | VERSION="dev-$(date +%Y%m%d-%H%M%S)" 21 | COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") 22 | BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") 23 | 24 | echo -e "${GREEN}========================================${NC}" 25 | echo -e "${GREEN}Docker 本地构建测试${NC}" 26 | echo -e "${GREEN}========================================${NC}" 27 | echo "" 28 | echo -e "${YELLOW}镜像信息:${NC}" 29 | echo " 名称: ${IMAGE_NAME}" 30 | echo " 标签: ${TAG}" 31 | echo " 版本: ${VERSION}" 32 | echo " 提交: ${COMMIT}" 33 | echo " 构建时间: ${BUILD_DATE}" 34 | echo "" 35 | 36 | # 检查 Docker 是否安装 37 | if ! command -v docker &> /dev/null; then 38 | echo -e "${RED}错误: Docker 未安装${NC}" 39 | exit 1 40 | fi 41 | 42 | # 构建选项 43 | BUILD_PLATFORM="linux/amd64" 44 | if [[ "$1" == "--multi-arch" ]]; then 45 | BUILD_PLATFORM="linux/amd64,linux/arm64,linux/arm/v7" 46 | echo -e "${YELLOW}多架构构建模式: ${BUILD_PLATFORM}${NC}" 47 | 48 | # 检查 buildx 是否可用 49 | if ! docker buildx version &> /dev/null; then 50 | echo -e "${RED}错误: Docker Buildx 未安装${NC}" 51 | echo "请运行: docker buildx install" 52 | exit 1 53 | fi 54 | else 55 | echo -e "${YELLOW}单架构构建模式: ${BUILD_PLATFORM}${NC}" 56 | fi 57 | 58 | echo "" 59 | echo -e "${GREEN}开始构建 Docker 镜像...${NC}" 60 | echo "" 61 | 62 | # 构建镜像 63 | if [[ "$1" == "--multi-arch" ]]; then 64 | # 多架构构建 65 | docker buildx build \ 66 | --platform ${BUILD_PLATFORM} \ 67 | --build-arg VERSION="${VERSION}" \ 68 | --build-arg COMMIT="${COMMIT}" \ 69 | --build-arg BUILD_DATE="${BUILD_DATE}" \ 70 | --build-arg TZ="Asia/Shanghai" \ 71 | -t ${IMAGE_NAME}:${TAG} \ 72 | -f Dockerfile \ 73 | --load \ 74 | . 75 | else 76 | # 单架构构建 77 | docker build \ 78 | --platform ${BUILD_PLATFORM} \ 79 | --build-arg VERSION="${VERSION}" \ 80 | --build-arg COMMIT="${COMMIT}" \ 81 | --build-arg BUILD_DATE="${BUILD_DATE}" \ 82 | --build-arg TZ="Asia/Shanghai" \ 83 | -t ${IMAGE_NAME}:${TAG} \ 84 | -f Dockerfile \ 85 | . 86 | fi 87 | 88 | if [ $? -eq 0 ]; then 89 | echo "" 90 | echo -e "${GREEN}========================================${NC}" 91 | echo -e "${GREEN}构建成功!${NC}" 92 | echo -e "${GREEN}========================================${NC}" 93 | echo "" 94 | 95 | # 显示镜像信息 96 | echo -e "${YELLOW}镜像详情:${NC}" 97 | docker images ${IMAGE_NAME}:${TAG} 98 | echo "" 99 | 100 | echo -e "${YELLOW}镜像大小分析:${NC}" 101 | docker image inspect ${IMAGE_NAME}:${TAG} --format='镜像大小: {{.Size}} bytes ({{ div .Size 1048576 }} MB)' 102 | echo "" 103 | 104 | # 提供运行命令 105 | echo -e "${GREEN}========================================${NC}" 106 | echo -e "${GREEN}测试运行命令:${NC}" 107 | echo -e "${GREEN}========================================${NC}" 108 | echo "" 109 | echo "1. 使用示例配置运行:" 110 | echo -e " ${YELLOW}docker run --rm -p 8900:8900 -v \$(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml ${IMAGE_NAME}:${TAG}${NC}" 111 | echo "" 112 | echo "2. 交互式运行(调试):" 113 | echo -e " ${YELLOW}docker run --rm -it -p 8900:8900 ${IMAGE_NAME}:${TAG} sh${NC}" 114 | echo "" 115 | echo "3. 后台运行:" 116 | echo -e " ${YELLOW}docker run -d --name sssd-test -p 8900:8900 -v \$(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml ${IMAGE_NAME}:${TAG}${NC}" 117 | echo "" 118 | echo "4. 查看日志:" 119 | echo -e " ${YELLOW}docker logs -f sssd-test${NC}" 120 | echo "" 121 | echo "5. 停止并删除容器:" 122 | echo -e " ${YELLOW}docker stop sssd-test && docker rm sssd-test${NC}" 123 | echo "" 124 | else 125 | echo "" 126 | echo -e "${RED}========================================${NC}" 127 | echo -e "${RED}构建失败!${NC}" 128 | echo -e "${RED}========================================${NC}" 129 | exit 1 130 | fi 131 | -------------------------------------------------------------------------------- /internal/dashboard/handler/config_api.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/ruanun/simple-server-status/internal/dashboard/config" 8 | "github.com/ruanun/simple-server-status/internal/dashboard/response" 9 | ) 10 | 11 | // ConfigProvider 配置提供者接口 12 | type ConfigProvider interface { 13 | GetConfig() *config.DashboardConfig 14 | } 15 | 16 | // LoggerProvider 日志提供者接口 17 | type LoggerProvider interface { 18 | Info(...interface{}) 19 | Infof(string, ...interface{}) 20 | } 21 | 22 | // ConfigValidationError 配置验证错误(从 internal 包复制以避免循环依赖) 23 | type ConfigValidationError struct { 24 | Field string `json:"field"` 25 | Value string `json:"value"` 26 | Message string `json:"message"` 27 | Level string `json:"level"` // error, warning, info 28 | } 29 | 30 | // ConfigValidatorProvider 配置验证器提供者接口 31 | type ConfigValidatorProvider interface { 32 | ValidateConfig(cfg *config.DashboardConfig) error 33 | GetValidationErrors() []ConfigValidationError 34 | GetErrorsByLevel(level string) []ConfigValidationError 35 | } 36 | 37 | // InitConfigAPI 初始化配置相关API 38 | func InitConfigAPI(group *gin.RouterGroup, configProvider ConfigProvider, logger LoggerProvider, validator ConfigValidatorProvider) { 39 | // 配置验证状态 40 | group.GET("/config/validation", getConfigValidation(validator, configProvider)) 41 | // 配置信息(脱敏) 42 | group.GET("/config/info", getConfigInfo(configProvider)) 43 | // 重新验证配置 44 | group.POST("/config/validate", validateConfig(validator, configProvider, logger)) 45 | } 46 | 47 | // getConfigValidation 获取配置验证状态 48 | func getConfigValidation(validator ConfigValidatorProvider, configProvider ConfigProvider) gin.HandlerFunc { 49 | return func(c *gin.Context) { 50 | // 获取当前配置 51 | cfg := configProvider.GetConfig() 52 | 53 | // 执行验证(忽略错误,因为验证结果通过 GetValidationErrors 获取) 54 | _ = validator.ValidateConfig(cfg) 55 | 56 | // 获取验证错误 57 | errors := validator.GetValidationErrors() 58 | 59 | // 统计各级别错误数量 60 | errorCount := len(validator.GetErrorsByLevel("error")) 61 | warningCount := len(validator.GetErrorsByLevel("warning")) 62 | infoCount := len(validator.GetErrorsByLevel("info")) 63 | 64 | // 返回验证结果 65 | data := gin.H{ 66 | "valid": errorCount == 0, 67 | "errors": errors, 68 | "error_count": errorCount, 69 | "warning_count": warningCount, 70 | "info_count": infoCount, 71 | } 72 | 73 | response.Success(c, data) 74 | } 75 | } 76 | 77 | // getConfigInfo 获取配置信息(脱敏) 78 | func getConfigInfo(configProvider ConfigProvider) gin.HandlerFunc { 79 | return func(c *gin.Context) { 80 | // 获取配置(线程安全) 81 | cfg := configProvider.GetConfig() 82 | 83 | // 创建脱敏的配置信息 84 | configInfo := gin.H{ 85 | "address": cfg.Address, 86 | "port": cfg.Port, 87 | "debug": cfg.Debug, 88 | "webSocketPath": cfg.WebSocketPath, 89 | "reportTimeIntervalMax": cfg.ReportTimeIntervalMax, 90 | "logPath": cfg.LogPath, 91 | "logLevel": cfg.LogLevel, 92 | "serverCount": len(cfg.Servers), 93 | } 94 | 95 | // 脱敏的服务器信息 96 | var servers []gin.H 97 | for _, server := range cfg.Servers { 98 | servers = append(servers, gin.H{ 99 | "id": server.Id, 100 | "name": server.Name, 101 | "group": server.Group, 102 | "countryCode": server.CountryCode, 103 | "hasSecret": server.Secret != "", 104 | "secretLength": len(server.Secret), 105 | }) 106 | } 107 | configInfo["servers"] = servers 108 | 109 | response.Success(c, configInfo) 110 | } 111 | } 112 | 113 | // validateConfig 重新验证配置 114 | func validateConfig(validator ConfigValidatorProvider, configProvider ConfigProvider, logger LoggerProvider) gin.HandlerFunc { 115 | return func(c *gin.Context) { 116 | // 获取当前配置 117 | cfg := configProvider.GetConfig() 118 | 119 | // 执行验证 120 | err := validator.ValidateConfig(cfg) 121 | 122 | // 获取验证错误 123 | errors := validator.GetValidationErrors() 124 | 125 | // 统计错误数量 126 | errorCount := len(validator.GetErrorsByLevel("error")) 127 | 128 | // 返回验证结果 129 | data := gin.H{ 130 | "valid": errorCount == 0, 131 | "errors": errors, 132 | "timestamp": time.Now(), 133 | } 134 | 135 | if err != nil { 136 | logger.Infof("配置验证完成,发现 %d 个错误", errorCount) 137 | } else { 138 | logger.Info("配置验证通过") 139 | } 140 | 141 | response.Success(c, data) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/shared/errors/types.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // ErrorType 错误类型枚举 9 | type ErrorType int 10 | 11 | const ( 12 | ErrorTypeNetwork ErrorType = iota 13 | ErrorTypeSystem 14 | ErrorTypeConfig 15 | ErrorTypeData 16 | ErrorTypeWebSocket 17 | ErrorTypeValidation 18 | ErrorTypeAuthentication 19 | ErrorTypeUnknown 20 | ) 21 | 22 | // ErrorTypeNames 错误类型名称映射 23 | var ErrorTypeNames = map[ErrorType]string{ 24 | ErrorTypeNetwork: "网络错误", 25 | ErrorTypeSystem: "系统错误", 26 | ErrorTypeConfig: "配置错误", 27 | ErrorTypeData: "数据错误", 28 | ErrorTypeWebSocket: "WebSocket错误", 29 | ErrorTypeValidation: "验证错误", 30 | ErrorTypeAuthentication: "认证错误", 31 | ErrorTypeUnknown: "未知错误", 32 | } 33 | 34 | // ErrorSeverity 错误严重程度 35 | type ErrorSeverity int 36 | 37 | const ( 38 | SeverityLow ErrorSeverity = iota 39 | SeverityMedium 40 | SeverityHigh 41 | SeverityCritical 42 | ) 43 | 44 | // SeverityNames 严重程度名称映射 45 | var SeverityNames = map[ErrorSeverity]string{ 46 | SeverityLow: "低", 47 | SeverityMedium: "中", 48 | SeverityHigh: "高", 49 | SeverityCritical: "严重", 50 | } 51 | 52 | // AppError 应用错误结构 53 | type AppError struct { 54 | Type ErrorType // 错误类型 55 | Severity ErrorSeverity // 严重程度 56 | Code string // 错误代码 57 | Message string // 错误消息 58 | Details string // 错误详情 59 | Cause error // 原始错误 60 | Timestamp time.Time // 发生时间 61 | Retryable bool // 是否可重试 62 | } 63 | 64 | // Error 实现 error 接口 65 | func (e *AppError) Error() string { 66 | if e.Cause != nil { 67 | return fmt.Sprintf("[%s] %s: %s (原因: %v)", e.Code, e.Message, e.Details, e.Cause) 68 | } 69 | if e.Details != "" { 70 | return fmt.Sprintf("[%s] %s: %s", e.Code, e.Message, e.Details) 71 | } 72 | return fmt.Sprintf("[%s] %s", e.Code, e.Message) 73 | } 74 | 75 | // NewAppError 创建新的应用错误 76 | func NewAppError(errType ErrorType, severity ErrorSeverity, code, message string) *AppError { 77 | return &AppError{ 78 | Type: errType, 79 | Severity: severity, 80 | Code: code, 81 | Message: message, 82 | Timestamp: time.Now(), 83 | Retryable: isRetryable(errType, severity), 84 | } 85 | } 86 | 87 | // WrapError 包装原始错误为应用错误 88 | func WrapError(err error, errType ErrorType, severity ErrorSeverity, code, message string) *AppError { 89 | if err == nil { 90 | return nil 91 | } 92 | return &AppError{ 93 | Type: errType, 94 | Severity: severity, 95 | Code: code, 96 | Message: message, 97 | Cause: err, 98 | Timestamp: time.Now(), 99 | Retryable: isRetryable(errType, severity), 100 | } 101 | } 102 | 103 | // WithDetails 添加错误详情 104 | func (e *AppError) WithDetails(details string) *AppError { 105 | e.Details = details 106 | return e 107 | } 108 | 109 | // WithCause 添加原始错误 110 | func (e *AppError) WithCause(cause error) *AppError { 111 | e.Cause = cause 112 | return e 113 | } 114 | 115 | // isRetryable 判断错误是否可重试 116 | func isRetryable(errType ErrorType, severity ErrorSeverity) bool { 117 | // 配置错误和认证错误通常不可重试 118 | if errType == ErrorTypeConfig || errType == ErrorTypeAuthentication { 119 | return false 120 | } 121 | 122 | // 严重错误不可重试 123 | if severity == SeverityCritical { 124 | return false 125 | } 126 | 127 | // 网络错误和系统错误可以重试 128 | return errType == ErrorTypeNetwork || errType == ErrorTypeSystem || errType == ErrorTypeWebSocket 129 | } 130 | 131 | // IsNetworkError 判断是否为网络相关错误 132 | func IsNetworkError(err error) bool { 133 | if err == nil { 134 | return false 135 | } 136 | // 检查常见的网络错误关键词 137 | msg := err.Error() 138 | keywords := []string{"connection", "network", "timeout", "dial", "refused", "reset"} 139 | for _, keyword := range keywords { 140 | if containsIgnoreCase(msg, keyword) { 141 | return true 142 | } 143 | } 144 | return false 145 | } 146 | 147 | // containsIgnoreCase 忽略大小写的字符串包含检查 148 | func containsIgnoreCase(s, substr string) bool { 149 | sLower := toLower(s) 150 | substrLower := toLower(substr) 151 | return contains(sLower, substrLower) 152 | } 153 | 154 | func toLower(s string) string { 155 | result := make([]byte, len(s)) 156 | for i := 0; i < len(s); i++ { 157 | c := s[i] 158 | if c >= 'A' && c <= 'Z' { 159 | c += 32 160 | } 161 | result[i] = c 162 | } 163 | return string(result) 164 | } 165 | 166 | func contains(s, substr string) bool { 167 | if len(substr) == 0 { 168 | return true 169 | } 170 | if len(s) < len(substr) { 171 | return false 172 | } 173 | for i := 0; i <= len(s)-len(substr); i++ { 174 | match := true 175 | for j := 0; j < len(substr); j++ { 176 | if s[i+j] != substr[j] { 177 | match = false 178 | break 179 | } 180 | } 181 | if match { 182 | return true 183 | } 184 | } 185 | return false 186 | } 187 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Simple Server Status Makefile 2 | # 作者: ruan 3 | 4 | .PHONY: help lint test build clean race coverage fmt vet install-tools 5 | 6 | # 默认目标 7 | .DEFAULT_GOAL := help 8 | 9 | # 颜色定义 10 | GREEN := \033[0;32m 11 | YELLOW := \033[0;33m 12 | RED := \033[0;31m 13 | NC := \033[0m 14 | 15 | # 变量定义 16 | BINARY_AGENT := sss-agent 17 | BINARY_DASHBOARD := sss-dashboard 18 | BIN_DIR := bin 19 | DIST_DIR := dist 20 | COVERAGE_FILE := coverage.out 21 | 22 | help: ## 显示帮助信息 23 | @echo "$(GREEN)Simple Server Status - 可用命令:$(NC)" 24 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-15s$(NC) %s\n", $$1, $$2}' 25 | 26 | install-tools: ## 安装开发工具 27 | @echo "$(GREEN)安装开发工具...$(NC)" 28 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 29 | go install github.com/securego/gosec/v2/cmd/gosec@latest 30 | go install golang.org/x/tools/cmd/goimports@latest 31 | @echo "$(GREEN)✓ 工具安装完成$(NC)" 32 | 33 | lint: ## 运行代码检查 34 | @echo "$(GREEN)运行 golangci-lint...$(NC)" 35 | golangci-lint run ./... 36 | 37 | lint-fix: ## 运行代码检查并自动修复 38 | @echo "$(GREEN)运行 golangci-lint (自动修复)...$(NC)" 39 | golangci-lint run --fix ./... 40 | 41 | fmt: ## 格式化代码 42 | @echo "$(GREEN)格式化代码...$(NC)" 43 | gofmt -s -w . 44 | goimports -w . 45 | 46 | vet: ## 运行 go vet 47 | @echo "$(GREEN)运行 go vet...$(NC)" 48 | go vet ./... 49 | 50 | test: ## 运行测试 51 | @echo "$(GREEN)运行测试...$(NC)" 52 | go test -v ./... 53 | 54 | test-coverage: ## 运行测试并生成覆盖率报告 55 | @echo "$(GREEN)运行测试并生成覆盖率...$(NC)" 56 | go test -v -coverprofile=$(COVERAGE_FILE) ./... 57 | go tool cover -html=$(COVERAGE_FILE) -o coverage.html 58 | @echo "$(GREEN)✓ 覆盖率报告: coverage.html$(NC)" 59 | 60 | race: ## 运行竞态检测 61 | @echo "$(GREEN)运行竞态检测...$(NC)" 62 | go test -race ./... 63 | 64 | build-web: ## 构建前端项目 65 | @echo "$(GREEN)构建前端项目...$(NC)" 66 | @bash scripts/build-web.sh 67 | 68 | build-agent: ## 构建 Agent 69 | @echo "$(GREEN)构建 Agent...$(NC)" 70 | @mkdir -p $(BIN_DIR) 71 | go build -o $(BIN_DIR)/$(BINARY_AGENT) ./cmd/agent 72 | @echo "$(GREEN)✓ Agent 构建完成: $(BIN_DIR)/$(BINARY_AGENT)$(NC)" 73 | 74 | build-dashboard: build-web ## 构建 Dashboard(包含前端) 75 | @echo "$(GREEN)构建 Dashboard...$(NC)" 76 | @mkdir -p $(BIN_DIR) 77 | go build -o $(BIN_DIR)/$(BINARY_DASHBOARD) ./cmd/dashboard 78 | @echo "$(GREEN)✓ Dashboard 构建完成: $(BIN_DIR)/$(BINARY_DASHBOARD)$(NC)" 79 | 80 | build-dashboard-only: ## 仅构建 Dashboard(不构建前端) 81 | @echo "$(GREEN)构建 Dashboard(跳过前端)...$(NC)" 82 | @mkdir -p $(BIN_DIR) 83 | go build -o $(BIN_DIR)/$(BINARY_DASHBOARD) ./cmd/dashboard 84 | @echo "$(GREEN)✓ Dashboard 构建完成: $(BIN_DIR)/$(BINARY_DASHBOARD)$(NC)" 85 | 86 | build: build-agent build-dashboard ## 构建所有二进制文件 87 | 88 | run-agent: build-agent ## 运行 Agent 89 | @echo "$(GREEN)运行 Agent...$(NC)" 90 | ./$(BIN_DIR)/$(BINARY_AGENT) 91 | 92 | run-dashboard: build-dashboard ## 运行 Dashboard 93 | @echo "$(GREEN)运行 Dashboard...$(NC)" 94 | ./$(BIN_DIR)/$(BINARY_DASHBOARD) 95 | 96 | dev-web: ## 启动前端开发服务器 97 | @echo "$(GREEN)启动前端开发服务器...$(NC)" 98 | cd web && pnpm run dev 99 | 100 | clean: ## 清理构建产物 101 | @echo "$(GREEN)清理构建产物...$(NC)" 102 | rm -rf $(BIN_DIR) $(DIST_DIR) $(COVERAGE_FILE) coverage.html 103 | rm -rf web/dist web/node_modules 104 | find internal/dashboard/public/dist -mindepth 1 ! -name '.gitkeep' ! -name 'README.md' -delete 2>/dev/null || true 105 | @echo "$(GREEN)✓ 清理完成$(NC)" 106 | 107 | clean-web: ## 清理前端构建产物 108 | @echo "$(GREEN)清理前端产物...$(NC)" 109 | rm -rf web/dist 110 | find internal/dashboard/public/dist -mindepth 1 ! -name '.gitkeep' ! -name 'README.md' -delete 2>/dev/null || true 111 | @echo "$(GREEN)✓ 前端清理完成$(NC)" 112 | 113 | tidy: ## 整理依赖 114 | @echo "$(GREEN)整理依赖...$(NC)" 115 | go mod tidy 116 | @echo "$(GREEN)✓ 依赖整理完成$(NC)" 117 | 118 | check: fmt vet lint test ## 运行所有检查(格式、审查、Lint、测试) 119 | 120 | gosec: ## 运行安全检查 121 | @echo "$(GREEN)运行安全检查...$(NC)" 122 | gosec -fmt=text ./... 123 | 124 | all: clean check build ## 清理、检查、构建全流程 125 | 126 | release: ## 使用 goreleaser 构建发布版本 127 | @echo "$(GREEN)使用 goreleaser 构建...$(NC)" 128 | goreleaser release --snapshot --clean 129 | 130 | pre-commit: fmt vet lint ## Git 提交前检查 131 | @echo "$(GREEN)✓ 提交前检查完成$(NC)" 132 | 133 | docker-build: ## 构建 Docker 镜像(本地测试) 134 | @echo "$(GREEN)构建 Docker 镜像...$(NC)" 135 | @bash scripts/build-docker.sh 136 | 137 | docker-build-multi: ## 构建多架构 Docker 镜像 138 | @echo "$(GREEN)构建多架构 Docker 镜像...$(NC)" 139 | @bash scripts/build-docker.sh --multi-arch 140 | 141 | docker-run: ## 运行 Docker 容器(使用示例配置) 142 | @echo "$(GREEN)运行 Docker 容器...$(NC)" 143 | docker run --rm -p 8900:8900 -v $$(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml sssd:dev 144 | 145 | docker-clean: ## 清理 Docker 镜像 146 | @echo "$(GREEN)清理 Docker 镜像...$(NC)" 147 | docker rmi sssd:dev 2>/dev/null || true 148 | @echo "$(GREEN)✓ Docker 镜像清理完成$(NC)" 149 | 150 | -------------------------------------------------------------------------------- /internal/agent/adaptive.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | 8 | "github.com/shirou/gopsutil/v4/cpu" 9 | "github.com/shirou/gopsutil/v4/mem" 10 | ) 11 | 12 | // AdaptiveCollector 自适应数据收集器 13 | type AdaptiveCollector struct { 14 | mu sync.RWMutex 15 | currentInterval time.Duration 16 | baseInterval time.Duration 17 | maxInterval time.Duration 18 | minInterval time.Duration 19 | lastCPUUsage float64 20 | lastMemUsage float64 21 | consecutiveHighLoad int 22 | consecutiveLowLoad int 23 | highLoadThreshold float64 24 | lowLoadThreshold float64 25 | adjustmentFactor float64 26 | logger interface { 27 | Warn(...interface{}) 28 | Infof(string, ...interface{}) 29 | Info(...interface{}) 30 | } 31 | } 32 | 33 | // NewAdaptiveCollector 创建新的自适应收集器 34 | func NewAdaptiveCollector(reportInterval int, logger interface { 35 | Warn(...interface{}) 36 | Infof(string, ...interface{}) 37 | Info(...interface{}) 38 | }) *AdaptiveCollector { 39 | baseInterval := time.Duration(reportInterval) * time.Second 40 | return &AdaptiveCollector{ 41 | currentInterval: baseInterval, 42 | baseInterval: baseInterval, 43 | maxInterval: (baseInterval * 5) / 2, // 最大间隔为基础间隔的2.5倍(5秒) 44 | minInterval: time.Second * 1, // 最小间隔1秒 45 | highLoadThreshold: 80.0, // CPU或内存使用率超过80%认为是高负载 46 | lowLoadThreshold: 30.0, // CPU或内存使用率低于30%认为是低负载 47 | adjustmentFactor: 1.2, // 调整因子 48 | logger: logger, 49 | } 50 | } 51 | 52 | // GetCurrentInterval 获取当前收集间隔 53 | func (ac *AdaptiveCollector) GetCurrentInterval() time.Duration { 54 | ac.mu.RLock() 55 | defer ac.mu.RUnlock() 56 | return ac.currentInterval 57 | } 58 | 59 | // UpdateInterval 根据系统负载更新收集间隔 60 | func (ac *AdaptiveCollector) UpdateInterval() { 61 | ac.mu.Lock() 62 | defer ac.mu.Unlock() 63 | 64 | // 获取CPU使用率 65 | cpuPercent, err := cpu.Percent(time.Second, false) 66 | if err != nil { 67 | ac.logger.Warn("Failed to get CPU usage:", err) 68 | return 69 | } 70 | 71 | // 获取内存使用率 72 | memInfo, err := mem.VirtualMemory() 73 | if err != nil { 74 | ac.logger.Warn("Failed to get memory usage:", err) 75 | return 76 | } 77 | 78 | currentCPU := cpuPercent[0] 79 | currentMem := memInfo.UsedPercent 80 | 81 | // 计算系统负载(CPU和内存使用率的最大值) 82 | systemLoad := math.Max(currentCPU, currentMem) 83 | 84 | // 根据负载调整间隔 85 | if systemLoad > ac.highLoadThreshold { 86 | // 高负载:增加收集间隔,减少系统压力 87 | ac.consecutiveHighLoad++ 88 | ac.consecutiveLowLoad = 0 89 | 90 | if ac.consecutiveHighLoad >= 3 { // 连续3次高负载才调整 91 | newInterval := time.Duration(float64(ac.currentInterval) * ac.adjustmentFactor) 92 | if newInterval <= ac.maxInterval { 93 | ac.currentInterval = newInterval 94 | ac.logger.Infof("High load detected (%.2f%%), increasing interval to %v", systemLoad, ac.currentInterval) 95 | } 96 | } 97 | } else if systemLoad < ac.lowLoadThreshold { 98 | // 低负载:减少收集间隔,提高数据精度 99 | ac.consecutiveLowLoad++ 100 | ac.consecutiveHighLoad = 0 101 | 102 | if ac.consecutiveLowLoad >= 5 { // 连续5次低负载才调整 103 | newInterval := time.Duration(float64(ac.currentInterval) / ac.adjustmentFactor) 104 | if newInterval >= ac.minInterval { 105 | ac.currentInterval = newInterval 106 | ac.logger.Infof("Low load detected (%.2f%%), decreasing interval to %v", systemLoad, ac.currentInterval) 107 | } 108 | } 109 | } else { 110 | // 中等负载:重置计数器,逐渐回归基础间隔 111 | ac.consecutiveHighLoad = 0 112 | ac.consecutiveLowLoad = 0 113 | 114 | // 如果当前间隔偏离基础间隔太多,逐渐调整回去 115 | if ac.currentInterval > ac.baseInterval { 116 | newInterval := time.Duration(float64(ac.currentInterval) * 0.95) 117 | if newInterval >= ac.baseInterval { 118 | ac.currentInterval = newInterval 119 | } else { 120 | ac.currentInterval = ac.baseInterval 121 | } 122 | } else if ac.currentInterval < ac.baseInterval { 123 | newInterval := time.Duration(float64(ac.currentInterval) * 1.05) 124 | if newInterval <= ac.baseInterval { 125 | ac.currentInterval = newInterval 126 | } else { 127 | ac.currentInterval = ac.baseInterval 128 | } 129 | } 130 | } 131 | 132 | // 更新历史数据 133 | ac.lastCPUUsage = currentCPU 134 | ac.lastMemUsage = currentMem 135 | } 136 | 137 | // GetLoadInfo 获取当前负载信息(用于调试) 138 | func (ac *AdaptiveCollector) GetLoadInfo() (float64, float64, time.Duration) { 139 | ac.mu.RLock() 140 | defer ac.mu.RUnlock() 141 | return ac.lastCPUUsage, ac.lastMemUsage, ac.currentInterval 142 | } 143 | 144 | // ResetToBase 重置到基础间隔 145 | func (ac *AdaptiveCollector) ResetToBase() { 146 | ac.mu.Lock() 147 | defer ac.mu.Unlock() 148 | ac.currentInterval = ac.baseInterval 149 | ac.consecutiveHighLoad = 0 150 | ac.consecutiveLowLoad = 0 151 | ac.logger.Info("Reset collection interval to base:", ac.baseInterval) 152 | } 153 | -------------------------------------------------------------------------------- /web/src/components/ServerInfoContent.vue: -------------------------------------------------------------------------------- 1 | 114 | 144 | 145 | 159 | -------------------------------------------------------------------------------- /scripts/build-docker.ps1: -------------------------------------------------------------------------------- 1 | # ============================================ 2 | # Docker 本地构建测试脚本 (PowerShell 版本) 3 | # 作者: ruan 4 | # 用途: 在 Windows 本地测试 Docker 镜像构建 5 | # ============================================ 6 | 7 | $ErrorActionPreference = "Stop" 8 | 9 | # 配置变量 10 | $ImageName = "sssd" 11 | $Tag = "dev" 12 | $Version = "dev-$(Get-Date -Format 'yyyyMMdd-HHmmss')" 13 | $Commit = & git rev-parse --short HEAD 2>$null 14 | if (-not $Commit) { $Commit = "unknown" } 15 | $BuildDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") 16 | 17 | Write-Host "========================================" -ForegroundColor Green 18 | Write-Host "Docker 本地构建测试" -ForegroundColor Green 19 | Write-Host "========================================" -ForegroundColor Green 20 | Write-Host "" 21 | Write-Host "镜像信息:" -ForegroundColor Yellow 22 | Write-Host " 名称: $ImageName" 23 | Write-Host " 标签: $Tag" 24 | Write-Host " 版本: $Version" 25 | Write-Host " 提交: $Commit" 26 | Write-Host " 构建时间: $BuildDate" 27 | Write-Host "" 28 | 29 | # 检查 Docker 是否安装 30 | try { 31 | $null = docker --version 32 | } catch { 33 | Write-Host "错误: Docker 未安装" -ForegroundColor Red 34 | exit 1 35 | } 36 | 37 | # 构建选项 38 | $BuildPlatform = "linux/amd64" 39 | $MultiArch = $args -contains "--multi-arch" 40 | 41 | if ($MultiArch) { 42 | $BuildPlatform = "linux/amd64,linux/arm64,linux/arm/v7" 43 | Write-Host "多架构构建模式: $BuildPlatform" -ForegroundColor Yellow 44 | 45 | # 检查 buildx 是否可用 46 | try { 47 | $null = docker buildx version 48 | } catch { 49 | Write-Host "错误: Docker Buildx 未安装" -ForegroundColor Red 50 | Write-Host "请运行: docker buildx install" 51 | exit 1 52 | } 53 | } else { 54 | Write-Host "单架构构建模式: $BuildPlatform" -ForegroundColor Yellow 55 | } 56 | 57 | Write-Host "" 58 | Write-Host "开始构建 Docker 镜像..." -ForegroundColor Green 59 | Write-Host "" 60 | 61 | # 构建镜像 62 | try { 63 | if ($MultiArch) { 64 | # 多架构构建 65 | docker buildx build ` 66 | --platform $BuildPlatform ` 67 | --build-arg VERSION="$Version" ` 68 | --build-arg COMMIT="$Commit" ` 69 | --build-arg BUILD_DATE="$BuildDate" ` 70 | --build-arg TZ="Asia/Shanghai" ` 71 | -t ${ImageName}:${Tag} ` 72 | -f Dockerfile ` 73 | --load ` 74 | . 75 | } else { 76 | # 单架构构建 77 | docker build ` 78 | --platform $BuildPlatform ` 79 | --build-arg VERSION="$Version" ` 80 | --build-arg COMMIT="$Commit" ` 81 | --build-arg BUILD_DATE="$BuildDate" ` 82 | --build-arg TZ="Asia/Shanghai" ` 83 | -t ${ImageName}:${Tag} ` 84 | -f Dockerfile ` 85 | . 86 | } 87 | 88 | Write-Host "" 89 | Write-Host "========================================" -ForegroundColor Green 90 | Write-Host "构建成功!" -ForegroundColor Green 91 | Write-Host "========================================" -ForegroundColor Green 92 | Write-Host "" 93 | 94 | # 显示镜像信息 95 | Write-Host "镜像详情:" -ForegroundColor Yellow 96 | docker images ${ImageName}:${Tag} 97 | Write-Host "" 98 | 99 | Write-Host "镜像大小分析:" -ForegroundColor Yellow 100 | $imageSize = docker image inspect ${ImageName}:${Tag} --format='{{.Size}}' 101 | $imageSizeMB = [math]::Round($imageSize / 1MB, 2) 102 | Write-Host "镜像大小: $imageSize bytes ($imageSizeMB MB)" 103 | Write-Host "" 104 | 105 | # 提供运行命令 106 | Write-Host "========================================" -ForegroundColor Green 107 | Write-Host "测试运行命令:" -ForegroundColor Green 108 | Write-Host "========================================" -ForegroundColor Green 109 | Write-Host "" 110 | Write-Host "1. 使用示例配置运行:" 111 | Write-Host " docker run --rm -p 8900:8900 -v `$(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml ${ImageName}:${Tag}" -ForegroundColor Yellow 112 | Write-Host "" 113 | Write-Host "2. 交互式运行(调试):" 114 | Write-Host " docker run --rm -it -p 8900:8900 ${ImageName}:${Tag} sh" -ForegroundColor Yellow 115 | Write-Host "" 116 | Write-Host "3. 后台运行:" 117 | Write-Host " docker run -d --name sssd-test -p 8900:8900 -v `$(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml ${ImageName}:${Tag}" -ForegroundColor Yellow 118 | Write-Host "" 119 | Write-Host "4. 查看日志:" 120 | Write-Host " docker logs -f sssd-test" -ForegroundColor Yellow 121 | Write-Host "" 122 | Write-Host "5. 停止并删除容器:" 123 | Write-Host " docker stop sssd-test; docker rm sssd-test" -ForegroundColor Yellow 124 | Write-Host "" 125 | 126 | } catch { 127 | Write-Host "" 128 | Write-Host "========================================" -ForegroundColor Red 129 | Write-Host "构建失败!" -ForegroundColor Red 130 | Write-Host "========================================" -ForegroundColor Red 131 | Write-Host $_.Exception.Message -ForegroundColor Red 132 | exit 1 133 | } 134 | -------------------------------------------------------------------------------- /web/src/composables/useConnectionManager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 连接管理 Composable 3 | * 4 | * 职责: 5 | * - 管理 WebSocket/HTTP 两种连接模式 6 | * - 处理连接初始化、切换、清理 7 | * - 管理 HTTP 轮询定时器 8 | * - 订阅 WebSocket 事件 9 | * 10 | * 使用方式: 11 | * ```typescript 12 | * import { useConnectionManager } from '@/composables/useConnectionManager' 13 | * const { initializeConnection, switchToHTTP, switchToWebSocket, cleanup } = useConnectionManager() 14 | * 15 | * // 初始化连接 16 | * onMounted(() => initializeConnection()) 17 | * 18 | * // 清理资源 19 | * onUnmounted(() => cleanup()) 20 | * ``` 21 | * 22 | * @author ruan 23 | */ 24 | 25 | import { onMounted, onUnmounted } from 'vue' 26 | import { message } from 'ant-design-vue' 27 | import { useWebSocket, WebSocketStatus } from '@/api/websocket' 28 | import { useConnectionStore } from '@/stores/connection' 29 | import { useServerStore } from '@/stores/server' 30 | import { serverService } from '@/services/serverService' 31 | import { useErrorHandler } from '@/composables/useErrorHandler' 32 | import { CONNECTION_MODES } from '@/constants/connectionModes' 33 | import type { ServerInfo } from '@/api/models' 34 | import { storeToRefs } from 'pinia' 35 | 36 | export function useConnectionManager() { 37 | const connectionStore = useConnectionStore() 38 | const serverStore = useServerStore() 39 | const { isWebSocketMode } = storeToRefs(connectionStore) 40 | const { client, connect, disconnect } = useWebSocket() 41 | const { handleHttpError } = useErrorHandler() 42 | 43 | // 轮询定时器 44 | let pollingTimer: number | null = null 45 | 46 | // ==================== 数据获取 ==================== 47 | 48 | /** 49 | * 获取服务器数据并更新 Store 50 | */ 51 | async function fetchServerData() { 52 | try { 53 | const data = await serverService.fetchServerInfo() 54 | serverStore.setServerData(data) 55 | } catch (error) { 56 | handleHttpError(error, '获取服务器信息') 57 | } 58 | } 59 | 60 | // ==================== HTTP 轮询管理 ==================== 61 | 62 | /** 63 | * 启动 HTTP 轮询 64 | * @param skipFirstLoad 是否跳过首次加载(默认:false) 65 | */ 66 | function startPolling(skipFirstLoad = false) { 67 | stopPolling() // 先停止现有轮询,避免重复 68 | if (!skipFirstLoad) { 69 | fetchServerData() 70 | } 71 | pollingTimer = window.setInterval(fetchServerData, 2000) 72 | } 73 | 74 | /** 75 | * 停止 HTTP 轮询 76 | */ 77 | function stopPolling() { 78 | if (pollingTimer) { 79 | clearInterval(pollingTimer) 80 | pollingTimer = null 81 | } 82 | } 83 | 84 | // ==================== 连接模式切换 ==================== 85 | 86 | /** 87 | * 切换到 HTTP 轮询模式 88 | */ 89 | function switchToHTTP() { 90 | disconnect() 91 | stopPolling() 92 | startPolling() 93 | message.info('已切换到 HTTP 轮询模式') 94 | } 95 | 96 | /** 97 | * 切换到 WebSocket 实时模式 98 | */ 99 | async function switchToWebSocket() { 100 | stopPolling() 101 | try { 102 | await connect() 103 | message.success('已切换到 WebSocket 实时模式') 104 | } catch (error) { 105 | message.error('WebSocket 连接失败,回退到 HTTP 轮询模式') 106 | connectionStore.setMode(CONNECTION_MODES.HTTP) 107 | startPolling() 108 | } 109 | } 110 | 111 | // ==================== 连接初始化 ==================== 112 | 113 | /** 114 | * 初始化连接 115 | * 根据 Connection Store 的模式决定使用 WebSocket 或 HTTP 轮询 116 | */ 117 | async function initializeConnection() { 118 | if (isWebSocketMode.value) { 119 | try { 120 | await connect() 121 | } catch (error) { 122 | console.error('WebSocket 连接失败,回退到 HTTP 轮询:', error) 123 | connectionStore.setMode(CONNECTION_MODES.HTTP) 124 | await fetchServerData() 125 | startPolling(true) // 跳过首次加载,因为上面已经加载过了 126 | } 127 | } else { 128 | await fetchServerData() 129 | startPolling(true) // 跳过首次加载,因为上面已经加载过了 130 | } 131 | } 132 | 133 | // ==================== 资源清理 ==================== 134 | 135 | /** 136 | * 清理所有资源 137 | * 包括停止轮询、断开 WebSocket、移除事件监听 138 | */ 139 | function cleanup() { 140 | stopPolling() 141 | disconnect() 142 | // 移除所有事件监听器 143 | client.off('onStatusUpdate') 144 | client.off('onConnectionChange') 145 | } 146 | 147 | // ==================== WebSocket 事件订阅 ==================== 148 | 149 | /** 150 | * 设置 WebSocket 事件监听 151 | * 在组件挂载时自动注册 152 | */ 153 | onMounted(() => { 154 | // 监听数据更新 155 | client.on('onStatusUpdate', (data: ServerInfo[]) => { 156 | serverStore.setServerData(data) 157 | }) 158 | 159 | // 监听连接状态变化 160 | client.on('onConnectionChange', (newStatus: WebSocketStatus) => { 161 | if (newStatus === WebSocketStatus.ERROR && isWebSocketMode.value) { 162 | // WebSocket 连接失败,自动切换到 HTTP 轮询模式 163 | message.warning('WebSocket 连接失败,自动切换到 HTTP 轮询模式') 164 | connectionStore.setMode(CONNECTION_MODES.HTTP) 165 | startPolling() 166 | } 167 | }) 168 | }) 169 | 170 | /** 171 | * 组件卸载时自动清理 172 | */ 173 | onUnmounted(() => { 174 | cleanup() 175 | }) 176 | 177 | // ==================== 导出 API ==================== 178 | 179 | return { 180 | // 连接管理 181 | initializeConnection, 182 | switchToHTTP, 183 | switchToWebSocket, 184 | cleanup, 185 | // 数据获取 186 | fetchServerData, 187 | // 轮询管理 188 | startPolling, 189 | stopPolling 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/agent/monitor.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "runtime" 6 | "sync" 7 | "time" 8 | 9 | "github.com/shirou/gopsutil/v4/cpu" 10 | "github.com/shirou/gopsutil/v4/mem" 11 | "github.com/shirou/gopsutil/v4/net" 12 | ) 13 | 14 | // PerformanceMetrics 性能指标结构 15 | type PerformanceMetrics struct { 16 | // 系统指标 17 | CPUUsage float64 `json:"cpu_usage"` 18 | MemoryUsage float64 `json:"memory_usage"` 19 | Goroutines int `json:"goroutines"` 20 | 21 | // 网络指标 22 | NetworkSent uint64 `json:"network_sent"` 23 | NetworkReceived uint64 `json:"network_received"` 24 | 25 | // 应用指标 26 | DataCollections int64 `json:"data_collections"` 27 | WebSocketMessages int64 `json:"websocket_messages"` 28 | Errors int64 `json:"errors"` 29 | Uptime float64 `json:"uptime_seconds"` 30 | LastUpdate time.Time `json:"last_update"` 31 | } 32 | 33 | // PerformanceMonitor 性能监控器 34 | type PerformanceMonitor struct { 35 | mu sync.RWMutex 36 | metrics *PerformanceMetrics 37 | startTime time.Time 38 | ctx context.Context 39 | cancel context.CancelFunc 40 | collectInterval time.Duration 41 | logInterval time.Duration 42 | logger interface{ Infof(string, ...interface{}) } 43 | 44 | // 计数器 45 | dataCollectionCount int64 46 | webSocketMessageCount int64 47 | errorCount int64 48 | 49 | // 网络基线 50 | lastNetworkSent uint64 51 | lastNetworkReceived uint64 52 | } 53 | 54 | // NewPerformanceMonitor 创建新的性能监控器 55 | func NewPerformanceMonitor(logger interface{ Infof(string, ...interface{}) }) *PerformanceMonitor { 56 | ctx, cancel := context.WithCancel(context.Background()) 57 | pm := &PerformanceMonitor{ 58 | metrics: &PerformanceMetrics{ 59 | LastUpdate: time.Now(), 60 | }, 61 | startTime: time.Now(), 62 | ctx: ctx, 63 | cancel: cancel, 64 | collectInterval: time.Second * 30, // 每30秒收集一次指标 65 | logInterval: time.Minute * 5, // 每5分钟记录一次日志 66 | logger: logger, 67 | } 68 | 69 | // 启动监控 70 | go pm.start() 71 | if logger != nil { 72 | logger.Infof("性能监控器已启动") 73 | } 74 | return pm 75 | } 76 | 77 | // start 启动监控循环 78 | func (pm *PerformanceMonitor) start() { 79 | collectTicker := time.NewTicker(pm.collectInterval) 80 | logTicker := time.NewTicker(pm.logInterval) 81 | defer collectTicker.Stop() 82 | defer logTicker.Stop() 83 | 84 | for { 85 | select { 86 | case <-pm.ctx.Done(): 87 | return 88 | case <-collectTicker.C: 89 | pm.collectMetrics() 90 | case <-logTicker.C: 91 | pm.logMetrics() 92 | } 93 | } 94 | } 95 | 96 | // collectMetrics 收集性能指标 97 | func (pm *PerformanceMonitor) collectMetrics() { 98 | pm.mu.Lock() 99 | defer pm.mu.Unlock() 100 | 101 | // 收集CPU使用率 102 | cpuPercent, err := cpu.Percent(time.Second, false) 103 | if err == nil && len(cpuPercent) > 0 { 104 | pm.metrics.CPUUsage = cpuPercent[0] 105 | } 106 | 107 | // 收集内存使用率 108 | vmStat, err := mem.VirtualMemory() 109 | if err == nil { 110 | pm.metrics.MemoryUsage = vmStat.UsedPercent 111 | } 112 | 113 | // 收集Goroutine数量 114 | pm.metrics.Goroutines = runtime.NumGoroutine() 115 | 116 | // 收集网络统计 117 | netStats, err := net.IOCounters(false) 118 | if err == nil && len(netStats) > 0 { 119 | currentSent := netStats[0].BytesSent 120 | currentReceived := netStats[0].BytesRecv 121 | 122 | if pm.lastNetworkSent > 0 { 123 | pm.metrics.NetworkSent = currentSent - pm.lastNetworkSent 124 | pm.metrics.NetworkReceived = currentReceived - pm.lastNetworkReceived 125 | } 126 | 127 | pm.lastNetworkSent = currentSent 128 | pm.lastNetworkReceived = currentReceived 129 | } 130 | 131 | // 更新应用指标 132 | pm.metrics.DataCollections = pm.dataCollectionCount 133 | pm.metrics.WebSocketMessages = pm.webSocketMessageCount 134 | pm.metrics.Errors = pm.errorCount 135 | pm.metrics.Uptime = time.Since(pm.startTime).Seconds() 136 | pm.metrics.LastUpdate = time.Now() 137 | } 138 | 139 | // logMetrics 记录性能指标到日志 140 | func (pm *PerformanceMonitor) logMetrics() { 141 | if pm.logger == nil { 142 | return 143 | } 144 | 145 | pm.mu.RLock() 146 | metrics := *pm.metrics // 复制一份避免长时间持锁 147 | pm.mu.RUnlock() 148 | 149 | pm.logger.Infof("性能指标 - CPU: %.2f%%, 内存: %.2f%%, Goroutines: %d, 运行时间: %.0fs", 150 | metrics.CPUUsage, metrics.MemoryUsage, metrics.Goroutines, metrics.Uptime) 151 | 152 | pm.logger.Infof("应用指标 - 数据收集: %d次, WebSocket消息: %d条, 错误: %d个", 153 | metrics.DataCollections, metrics.WebSocketMessages, metrics.Errors) 154 | 155 | if metrics.NetworkSent > 0 || metrics.NetworkReceived > 0 { 156 | pm.logger.Infof("网络指标 - 发送: %d字节, 接收: %d字节", 157 | metrics.NetworkSent, metrics.NetworkReceived) 158 | } 159 | } 160 | 161 | // GetMetrics 获取当前性能指标 162 | func (pm *PerformanceMonitor) GetMetrics() *PerformanceMetrics { 163 | pm.mu.RLock() 164 | defer pm.mu.RUnlock() 165 | 166 | // 返回指标的副本 167 | metricsCopy := *pm.metrics 168 | return &metricsCopy 169 | } 170 | 171 | // IncrementDataCollection 增加数据收集计数 172 | func (pm *PerformanceMonitor) IncrementDataCollection() { 173 | pm.mu.Lock() 174 | pm.dataCollectionCount++ 175 | pm.mu.Unlock() 176 | } 177 | 178 | // IncrementWebSocketMessage 增加WebSocket消息计数 179 | func (pm *PerformanceMonitor) IncrementWebSocketMessage() { 180 | pm.mu.Lock() 181 | pm.webSocketMessageCount++ 182 | pm.mu.Unlock() 183 | } 184 | 185 | // IncrementError 增加错误计数 186 | func (pm *PerformanceMonitor) IncrementError() { 187 | pm.mu.Lock() 188 | pm.errorCount++ 189 | pm.mu.Unlock() 190 | } 191 | 192 | // Close 关闭性能监控器 193 | func (pm *PerformanceMonitor) Close() { 194 | pm.cancel() 195 | if pm.logger != nil { 196 | pm.logger.Infof("性能监控器已关闭") 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # GoReleaser 配置文件 2 | # 文档: https://goreleaser.com 3 | # 作者: ruanun 4 | # 版本: 2.0.0 5 | 6 | version: 2 7 | 8 | before: 9 | hooks: 10 | # 在构建前确保依赖是最新的 11 | - go mod tidy 12 | # 注意:前端构建已在 GitHub Actions 工作流中通过 scripts/build-web.sh 完成 13 | # 本地使用时,请先运行: make build-web 或 bash scripts/build-web.sh 14 | 15 | builds: 16 | # Agent 构建配置 17 | - id: agent 18 | main: ./cmd/agent 19 | binary: sss-agent 20 | env: 21 | - CGO_ENABLED=0 22 | goos: 23 | - linux 24 | - windows 25 | - darwin 26 | - freebsd 27 | goarch: 28 | - amd64 29 | - arm64 30 | - arm 31 | goarm: 32 | - "7" 33 | ignore: 34 | - goos: windows 35 | goarch: arm 36 | - goos: windows 37 | goarch: arm64 38 | - goos: darwin 39 | goarch: arm 40 | - goos: freebsd 41 | goarch: arm 42 | ldflags: 43 | - -s -w 44 | - -X main.version={{.Version}} 45 | - -X main.commit={{.Commit}} 46 | - -X main.date={{.Date}} 47 | - -X main.builtBy=goreleaser 48 | flags: 49 | - -trimpath 50 | 51 | # Dashboard 构建配置 52 | - id: dashboard 53 | main: ./cmd/dashboard 54 | binary: sss-dashboard 55 | env: 56 | - CGO_ENABLED=0 57 | goos: 58 | - linux 59 | - windows 60 | - darwin 61 | - freebsd 62 | goarch: 63 | - amd64 64 | - arm64 65 | - arm 66 | goarm: 67 | - "7" 68 | ignore: 69 | - goos: windows 70 | goarch: arm 71 | - goos: windows 72 | goarch: arm64 73 | - goos: darwin 74 | goarch: arm 75 | - goos: freebsd 76 | goarch: arm 77 | ldflags: 78 | - -s -w 79 | - -X main.version={{.Version}} 80 | - -X main.commit={{.Commit}} 81 | - -X main.date={{.Date}} 82 | - -X main.builtBy=goreleaser 83 | flags: 84 | - -trimpath 85 | 86 | archives: 87 | # Agent 独立包 88 | - id: agent 89 | ids: [agent] 90 | formats: 91 | - tar.gz 92 | wrap_in_directory: true 93 | name_template: >- 94 | sss-agent_ 95 | {{- .Version }}_ 96 | {{- .Os }}_ 97 | {{- .Arch }} 98 | {{- if .Arm }}v{{ .Arm }}{{ end }} 99 | format_overrides: 100 | - goos: windows 101 | formats: 102 | - zip 103 | files: 104 | - README.md 105 | - LICENSE 106 | - configs/sss-agent.yaml.example 107 | - deployments/systemd/sssa.service 108 | 109 | # Dashboard 独立包 110 | - id: dashboard 111 | ids: [dashboard] 112 | formats: 113 | - tar.gz 114 | wrap_in_directory: true 115 | name_template: >- 116 | sss-dashboard_ 117 | {{- .Version }}_ 118 | {{- .Os }}_ 119 | {{- .Arch }} 120 | {{- if .Arm }}v{{ .Arm }}{{ end }} 121 | format_overrides: 122 | - goos: windows 123 | formats: 124 | - zip 125 | files: 126 | - README.md 127 | - LICENSE 128 | - configs/sss-dashboard.yaml.example 129 | 130 | checksum: 131 | name_template: 'checksums.txt' 132 | algorithm: sha256 133 | 134 | snapshot: 135 | version_template: "{{ incpatch .Version }}-next" 136 | 137 | changelog: 138 | sort: asc 139 | use: github 140 | filters: 141 | exclude: 142 | - '^docs:' 143 | - '^test:' 144 | - '^chore:' 145 | - '^ci:' 146 | - 'merge conflict' 147 | - Merge pull request 148 | - Merge remote-tracking branch 149 | - Merge branch 150 | groups: 151 | - title: '🎉 新功能' 152 | regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' 153 | order: 0 154 | - title: '🐛 Bug 修复' 155 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 156 | order: 1 157 | - title: '📝 文档更新' 158 | regexp: '^.*?docs(\([[:word:]]+\))??!?:.+$' 159 | order: 2 160 | - title: '🚀 性能优化' 161 | regexp: '^.*?perf(\([[:word:]]+\))??!?:.+$' 162 | order: 3 163 | - title: '♻️ 代码重构' 164 | regexp: '^.*?refactor(\([[:word:]]+\))??!?:.+$' 165 | order: 4 166 | - title: '其他更改' 167 | order: 999 168 | 169 | release: 170 | github: 171 | owner: ruanun 172 | name: simple-server-status 173 | draft: false 174 | prerelease: auto 175 | mode: replace 176 | name_template: "{{.ProjectName}} v{{.Version}}" 177 | header: | 178 | ## Simple Server Status v{{.Version}} 179 | 180 | 🎉 欢迎使用 Simple Server Status! 181 | 182 | ### 快速安装 183 | 184 | **Agent 一键安装:** 185 | ```bash 186 | # Linux/macOS/FreeBSD 187 | curl -fsSL https://raw.githubusercontent.com/ruanun/simple-server-status/main/scripts/install-agent.sh | bash 188 | 189 | # Windows (PowerShell) 190 | iwr -useb https://raw.githubusercontent.com/ruanun/simple-server-status/main/scripts/install-agent.ps1 | iex 191 | ``` 192 | 193 | **Dashboard Docker 部署:** 194 | ```bash 195 | docker run --name sssd -d -p 8900:8900 -v ./sss-dashboard.yaml:/app/sss-dashboard.yaml ruanun/sssd:{{.Version}} 196 | ``` 197 | 198 | ### 📦 下载说明 199 | 200 | - `sss-agent`: 监控代理客户端 201 | - `sss-dashboard`: 监控面板服务端 202 | 203 | 请根据你的操作系统和架构选择合适的版本下载。 204 | 205 | footer: | 206 | --- 207 | 208 | **完整文档:** https://github.com/ruanun/simple-server-status/blob/master/README.md 209 | 210 | **问题反馈:** https://github.com/ruanun/simple-server-status/issues 211 | 212 | 感谢使用 Simple Server Status! ⭐ 213 | 214 | # 注释掉 nfpms 部分,如果需要生成 Linux 包可以取消注释 215 | # nfpms: 216 | # - id: default 217 | # package_name: simple-server-status 218 | # homepage: https://github.com/ruanun/simple-server-status 219 | # maintainer: ruan 220 | # description: 极简服务器监控探针 221 | # license: MIT 222 | # formats: 223 | # - deb 224 | # - rpm 225 | # bindir: /usr/local/bin 226 | -------------------------------------------------------------------------------- /internal/shared/errors/handler.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // RetryConfig 重试配置 13 | type RetryConfig struct { 14 | MaxAttempts int // 最大重试次数 15 | InitialDelay time.Duration // 初始延迟 16 | MaxDelay time.Duration // 最大延迟 17 | BackoffFactor float64 // 退避因子 18 | Timeout time.Duration // 超时时间 19 | } 20 | 21 | // DefaultRetryConfig 默认重试配置 22 | func DefaultRetryConfig() *RetryConfig { 23 | return &RetryConfig{ 24 | MaxAttempts: 3, 25 | InitialDelay: time.Second, 26 | MaxDelay: time.Minute, 27 | BackoffFactor: 2.0, 28 | Timeout: time.Minute * 5, 29 | } 30 | } 31 | 32 | // ErrorHandler 错误处理器 33 | type ErrorHandler struct { 34 | logger *zap.SugaredLogger 35 | retryConfig *RetryConfig 36 | errorStats map[ErrorType]int64 37 | lastErrors []*AppError 38 | maxLastErrors int 39 | mu sync.RWMutex 40 | } 41 | 42 | // NewErrorHandler 创建新的错误处理器 43 | func NewErrorHandler(logger *zap.SugaredLogger, config *RetryConfig) *ErrorHandler { 44 | if config == nil { 45 | config = DefaultRetryConfig() 46 | } 47 | return &ErrorHandler{ 48 | logger: logger, 49 | retryConfig: config, 50 | errorStats: make(map[ErrorType]int64), 51 | lastErrors: make([]*AppError, 0), 52 | maxLastErrors: 100, 53 | } 54 | } 55 | 56 | // HandleError 处理错误 57 | func (eh *ErrorHandler) HandleError(err *AppError) { 58 | eh.mu.Lock() 59 | defer eh.mu.Unlock() 60 | 61 | // 记录错误统计 62 | eh.errorStats[err.Type]++ 63 | 64 | // 保存最近的错误 65 | eh.lastErrors = append(eh.lastErrors, err) 66 | if len(eh.lastErrors) > eh.maxLastErrors { 67 | eh.lastErrors = eh.lastErrors[1:] 68 | } 69 | 70 | // 根据严重程度记录日志 71 | eh.logError(err) 72 | } 73 | 74 | // logError 记录错误日志 75 | func (eh *ErrorHandler) logError(err *AppError) { 76 | if eh.logger == nil { 77 | return 78 | } 79 | 80 | logMsg := fmt.Sprintf("[%s] %s", ErrorTypeNames[err.Type], err.Error()) 81 | 82 | switch err.Severity { 83 | case SeverityLow: 84 | eh.logger.Debug(logMsg) 85 | case SeverityMedium: 86 | eh.logger.Warn(logMsg) 87 | case SeverityHigh: 88 | eh.logger.Error(logMsg) 89 | case SeverityCritical: 90 | eh.logger.Error("⚠️ 严重错误: ", logMsg) 91 | } 92 | } 93 | 94 | // RetryWithBackoff 带指数退避的重试机制 95 | func (eh *ErrorHandler) RetryWithBackoff(ctx context.Context, operation func() error, errType ErrorType) error { 96 | var lastErr error 97 | delay := eh.retryConfig.InitialDelay 98 | 99 | for attempt := 1; attempt <= eh.retryConfig.MaxAttempts; attempt++ { 100 | // 检查上下文是否已取消 101 | select { 102 | case <-ctx.Done(): 103 | return ctx.Err() 104 | default: 105 | } 106 | 107 | // 执行操作 108 | err := operation() 109 | if err == nil { 110 | // 成功,如果之前有失败记录日志 111 | if attempt > 1 && eh.logger != nil { 112 | eh.logger.Infof("操作在第 %d 次尝试后成功", attempt) 113 | } 114 | return nil 115 | } 116 | 117 | lastErr = err 118 | 119 | // 创建应用错误 120 | appErr := WrapError(err, errType, SeverityMedium, 121 | "RETRY_FAILED", 122 | fmt.Sprintf("操作失败 (尝试 %d/%d)", attempt, eh.retryConfig.MaxAttempts)) 123 | eh.HandleError(appErr) 124 | 125 | // 如果不可重试或已达到最大重试次数,直接返回 126 | if !appErr.Retryable || attempt == eh.retryConfig.MaxAttempts { 127 | break 128 | } 129 | 130 | // 等待后重试 131 | select { 132 | case <-ctx.Done(): 133 | return ctx.Err() 134 | case <-time.After(delay): 135 | // 计算下次延迟时间(指数退避) 136 | delay = time.Duration(float64(delay) * eh.retryConfig.BackoffFactor) 137 | if delay > eh.retryConfig.MaxDelay { 138 | delay = eh.retryConfig.MaxDelay 139 | } 140 | } 141 | } 142 | 143 | return lastErr 144 | } 145 | 146 | // GetErrorStats 获取错误统计 147 | func (eh *ErrorHandler) GetErrorStats() map[ErrorType]int64 { 148 | eh.mu.RLock() 149 | defer eh.mu.RUnlock() 150 | stats := make(map[ErrorType]int64) 151 | for k, v := range eh.errorStats { 152 | stats[k] = v 153 | } 154 | return stats 155 | } 156 | 157 | // GetRecentErrors 获取最近的错误 158 | func (eh *ErrorHandler) GetRecentErrors(count int) []*AppError { 159 | eh.mu.RLock() 160 | defer eh.mu.RUnlock() 161 | 162 | if count <= 0 || count > len(eh.lastErrors) { 163 | count = len(eh.lastErrors) 164 | } 165 | 166 | if count == 0 { 167 | return []*AppError{} 168 | } 169 | 170 | start := len(eh.lastErrors) - count 171 | result := make([]*AppError, count) 172 | copy(result, eh.lastErrors[start:]) 173 | return result 174 | } 175 | 176 | // LogErrorStats 记录错误统计信息 177 | func (eh *ErrorHandler) LogErrorStats() { 178 | if eh.logger == nil { 179 | return 180 | } 181 | 182 | stats := eh.GetErrorStats() 183 | eh.logger.Infof("错误统计 - 网络: %d, 系统: %d, 配置: %d, 数据: %d, WebSocket: %d, 验证: %d, 认证: %d, 未知: %d", 184 | stats[ErrorTypeNetwork], 185 | stats[ErrorTypeSystem], 186 | stats[ErrorTypeConfig], 187 | stats[ErrorTypeData], 188 | stats[ErrorTypeWebSocket], 189 | stats[ErrorTypeValidation], 190 | stats[ErrorTypeAuthentication], 191 | stats[ErrorTypeUnknown]) 192 | } 193 | 194 | // SafeExecute 安全执行函数,捕获 panic 并转换为错误 195 | func SafeExecute(operation func() error, errType ErrorType, description string) (err error) { 196 | defer func() { 197 | if r := recover(); r != nil { 198 | err = NewAppError(errType, SeverityCritical, "PANIC", 199 | fmt.Sprintf("Panic 发生在 %s: %v", description, r)) 200 | } 201 | }() 202 | 203 | return operation() 204 | } 205 | 206 | // SafeGo 安全启动 goroutine,捕获 panic 207 | func SafeGo(handler *ErrorHandler, fn func(), description string) { 208 | go func() { 209 | defer func() { 210 | if r := recover(); r != nil { 211 | panicErr := NewAppError(ErrorTypeSystem, SeverityCritical, "GOROUTINE_PANIC", 212 | fmt.Sprintf("Goroutine panic: %s", description)). 213 | WithDetails(fmt.Sprintf("%v", r)) 214 | if handler != nil { 215 | handler.HandleError(panicErr) 216 | } 217 | } 218 | }() 219 | fn() 220 | }() 221 | } 222 | -------------------------------------------------------------------------------- /docs/development/docker-build.md: -------------------------------------------------------------------------------- 1 | # Docker 构建指南 2 | 3 | 本项目使用多阶段构建 Dockerfile,支持完全自包含的前后端构建流程。 4 | 5 | ## 📋 目录 6 | 7 | - [快速开始](#快速开始) 8 | - [构建方式对比](#构建方式对比) 9 | - [本地构建](#本地构建) 10 | - [CI/CD 构建](#cicd-构建) 11 | - [多架构支持](#多架构支持) 12 | - [常见问题](#常见问题) 13 | 14 | ## 🚀 快速开始 15 | 16 | ### 方式 1:使用 Make 命令(推荐) 17 | 18 | ```bash 19 | # 构建 Docker 镜像 20 | make docker-build 21 | 22 | # 运行容器 23 | make docker-run 24 | 25 | # 清理镜像 26 | make docker-clean 27 | ``` 28 | 29 | ### 方式 2:使用脚本 30 | 31 | **Linux/macOS:** 32 | ```bash 33 | # 单架构构建 34 | bash scripts/build-docker.sh 35 | 36 | # 多架构构建 37 | bash scripts/build-docker.sh --multi-arch 38 | ``` 39 | 40 | **Windows (PowerShell):** 41 | ```powershell 42 | # 单架构构建 43 | .\scripts\build-docker.ps1 44 | 45 | # 多架构构建 46 | .\scripts\build-docker.ps1 --multi-arch 47 | ``` 48 | 49 | ### 方式 3:直接使用 Docker 命令 50 | 51 | ```bash 52 | # 基础构建 53 | docker build -t sssd:dev -f Dockerfile . 54 | 55 | # 带参数构建 56 | docker build \ 57 | --build-arg VERSION=v1.0.0 \ 58 | --build-arg COMMIT=$(git rev-parse --short HEAD) \ 59 | --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 60 | -t sssd:dev \ 61 | -f Dockerfile \ 62 | . 63 | ``` 64 | 65 | ## 📊 构建方式对比 66 | 67 | | 方式 | 优点 | 缺点 | 适用场景 | 68 | |------|------|------|---------| 69 | | **多阶段 Dockerfile** (当前) | 完全自包含、镜像小、易维护 | 构建时间稍长 | ✅ 推荐用于所有场景 | 70 | | GoReleaser + Docker | 一体化发布流程 | 配置复杂 | CI/CD 自动化发布 | 71 | | 简单 Dockerfile | 构建快速 | 依赖外部编译 | 已有编译产物 | 72 | 73 | ## 🛠️ 本地构建 74 | 75 | ### 构建参数说明 76 | 77 | Dockerfile 支持以下构建参数: 78 | 79 | | 参数 | 默认值 | 说明 | 80 | |------|--------|------| 81 | | `VERSION` | `dev` | 版本号 | 82 | | `COMMIT` | `unknown` | Git 提交哈希 | 83 | | `BUILD_DATE` | `unknown` | 构建时间 | 84 | | `TZ` | `Asia/Shanghai` | 时区设置 | 85 | 86 | ### 完整构建示例 87 | 88 | ```bash 89 | docker build \ 90 | --build-arg VERSION=v1.2.3 \ 91 | --build-arg COMMIT=$(git rev-parse --short HEAD) \ 92 | --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \ 93 | --build-arg TZ=Asia/Shanghai \ 94 | -t sssd:v1.2.3 \ 95 | -f Dockerfile \ 96 | . 97 | ``` 98 | 99 | ### 运行容器 100 | 101 | **使用示例配置:** 102 | ```bash 103 | docker run --rm -p 8900:8900 \ 104 | -v $(pwd)/configs/sss-dashboard.yaml.example:/app/sss-dashboard.yaml \ 105 | sssd:dev 106 | ``` 107 | 108 | **挂载自定义配置:** 109 | ```bash 110 | docker run -d \ 111 | --name sssd \ 112 | -p 8900:8900 \ 113 | -v /path/to/your/config.yaml:/app/sss-dashboard.yaml \ 114 | -v /path/to/logs:/app/.logs \ 115 | --restart=unless-stopped \ 116 | sssd:dev 117 | ``` 118 | 119 | ## 🔄 CI/CD 构建 120 | 121 | 项目使用 GitHub Actions 自动构建和推送 Docker 镜像。 122 | 123 | ### 工作流程 124 | 125 | 1. **触发条件**: 推送 tag(如 `v1.0.0`) 126 | 2. **构建流程**: 127 | - GoReleaser 编译二进制文件(多平台) 128 | - Docker Buildx 构建镜像(多架构) 129 | - 推送到 Docker Hub 130 | 131 | ### 自动构建的镜像标签 132 | 133 | - `ruanun/sssd:v1.0.0` - 完整版本号 134 | - `ruanun/sssd:1.0` - 主版本号 135 | - `ruanun/sssd:1` - 大版本号 136 | - `ruanun/sssd:latest` - 最新版本 137 | 138 | ### GitHub Actions 配置 139 | 140 | 参考 `.github/workflows/release.yml`: 141 | 142 | ```yaml 143 | - name: 构建并推送 Docker 镜像 144 | uses: docker/build-push-action@v5 145 | with: 146 | context: . 147 | file: ./Dockerfile 148 | platforms: linux/amd64,linux/arm64,linux/arm/v7 149 | push: true 150 | build-args: | 151 | VERSION=${{ github.ref_name }} 152 | COMMIT=${{ github.sha }} 153 | BUILD_DATE=${{ github.event.repository.updated_at }} 154 | ``` 155 | 156 | ## 🌐 多架构支持 157 | 158 | 项目支持以下平台架构: 159 | 160 | - `linux/amd64` - x86_64 架构(PC、服务器) 161 | - `linux/arm64` - ARM64 架构(Apple Silicon、ARM 服务器) 162 | - `linux/arm/v7` - ARMv7 架构(树莓派 3/4) 163 | 164 | ### 使用 Buildx 构建多架构镜像 165 | 166 | ```bash 167 | # 创建 builder(首次) 168 | docker buildx create --name multiarch --use 169 | 170 | # 构建并推送多架构镜像 171 | docker buildx build \ 172 | --platform linux/amd64,linux/arm64,linux/arm/v7 \ 173 | --build-arg VERSION=v1.0.0 \ 174 | -t username/sssd:v1.0.0 \ 175 | -f Dockerfile \ 176 | --push \ 177 | . 178 | ``` 179 | 180 | ## 📦 镜像优化 181 | 182 | ### 镜像大小 183 | 184 | - **最终镜像大小**: ~30 MB 185 | - **基础镜像**: Alpine Linux (轻量级) 186 | - **优化措施**: 187 | - 多阶段构建(分离构建和运行环境) 188 | - 静态编译(无 CGO 依赖) 189 | - 清理不必要文件 190 | 191 | ### 安全性 192 | 193 | - ✅ 使用非 root 用户运行 194 | - ✅ 最小化运行时依赖 195 | - ✅ 定期更新基础镜像 196 | - ✅ 健康检查配置 197 | 198 | ### 健康检查 199 | 200 | Dockerfile 内置健康检查: 201 | 202 | ```dockerfile 203 | HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ 204 | CMD wget --quiet --tries=1 --spider http://localhost:8900/api/statistics || exit 1 205 | ``` 206 | 207 | ## ❓ 常见问题 208 | 209 | ### Q1: 构建失败:前端依赖安装错误 210 | 211 | **解决方案:** 212 | ```bash 213 | # 清理 web/node_modules 214 | rm -rf web/node_modules 215 | 216 | # 重新构建 217 | docker build --no-cache -t sssd:dev -f Dockerfile . 218 | ``` 219 | 220 | ### Q2: 镜像体积过大 221 | 222 | **检查镜像层:** 223 | ```bash 224 | docker history sssd:dev 225 | ``` 226 | 227 | **确保使用多阶段构建的最终阶段:** 228 | ```dockerfile 229 | FROM alpine:latest # 最终阶段 230 | ``` 231 | 232 | ### Q3: 多架构构建失败 233 | 234 | **安装 QEMU 模拟器:** 235 | ```bash 236 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 237 | ``` 238 | 239 | **重新创建 builder:** 240 | ```bash 241 | docker buildx rm multiarch 242 | docker buildx create --name multiarch --use 243 | docker buildx inspect --bootstrap 244 | ``` 245 | 246 | ### Q4: 容器启动后立即退出 247 | 248 | **查看日志:** 249 | ```bash 250 | docker logs 251 | ``` 252 | 253 | **常见原因:** 254 | - 配置文件路径错误 255 | - 配置文件格式错误 256 | - 端口被占用 257 | 258 | **调试模式运行:** 259 | ```bash 260 | docker run --rm -it sssd:dev sh 261 | ``` 262 | 263 | ### Q5: 如何查看镜像构建参数 264 | 265 | ```bash 266 | docker inspect sssd:dev | grep -A 10 "Labels" 267 | ``` 268 | 269 | ## 📚 相关文档 270 | 271 | - [Dockerfile 最佳实践](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) 272 | - [多阶段构建](https://docs.docker.com/build/building/multi-stage/) 273 | - [Docker Buildx](https://docs.docker.com/buildx/working-with-buildx/) 274 | - [部署文档](../deployment/docker.md) 275 | 276 | ## 🤝 贡献 277 | 278 | 如果你有改进 Docker 构建的建议,欢迎提交 Issue 或 Pull Request! 279 | 280 | --- 281 | 282 | **作者**: ruan 283 | **更新时间**: 2025-01-15 284 | -------------------------------------------------------------------------------- /web/src/pages/StatusPage.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 106 | 107 | 178 | -------------------------------------------------------------------------------- /internal/agent/gopsutil.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/ruanun/simple-server-status/pkg/model" 8 | "github.com/shirou/gopsutil/v4/cpu" 9 | "github.com/shirou/gopsutil/v4/disk" 10 | "github.com/shirou/gopsutil/v4/host" 11 | "github.com/shirou/gopsutil/v4/load" 12 | "github.com/shirou/gopsutil/v4/mem" 13 | ) 14 | 15 | // GetServerInfo 获取服务器信息 16 | // hostIp: 服务器IP地址(可选,传空字符串表示未设置) 17 | // hostLocation: 服务器地理位置(可选,传空字符串表示未设置) 18 | func GetServerInfo(hostIp, hostLocation string) *model.ServerInfo { 19 | return &model.ServerInfo{ 20 | //Name: "win", 21 | HostInfo: getHostInfo(), 22 | CpuInfo: getCpuInfo(), 23 | VirtualMemoryInfo: getMemInfo(), 24 | SwapMemoryInfo: getSwapMemInfo(), 25 | DiskInfo: getDiskInfo(), 26 | NetworkInfo: getNetInfo(), 27 | 28 | Ip: hostIp, 29 | Loc: hostLocation, 30 | } 31 | } 32 | 33 | // @brief:耗时统计函数 34 | func timeCost() func() { 35 | //start := time.Now() 36 | return func() { 37 | //tc := time.Since(start) 38 | //fmt.Printf("time cost = %v\n", tc) 39 | } 40 | } 41 | 42 | func getHostInfo() *model.HostInfo { 43 | defer timeCost()() 44 | 45 | info, err := host.Info() 46 | if err != nil { 47 | fmt.Println("get host info fail, error: ", err) 48 | } 49 | var hostInfo model.HostInfo 50 | hostInfo.KernelArch = info.KernelArch 51 | hostInfo.KernelVersion = info.KernelVersion 52 | hostInfo.VirtualizationSystem = info.VirtualizationSystem 53 | hostInfo.Uptime = info.Uptime 54 | hostInfo.BootTime = info.BootTime 55 | hostInfo.OS = info.OS 56 | hostInfo.Platform = info.Platform 57 | hostInfo.PlatformVersion = info.PlatformVersion 58 | hostInfo.PlatformFamily = info.PlatformFamily 59 | 60 | loadInfo, err := load.Avg() 61 | if err != nil { 62 | fmt.Println("get average load fail. err: ", err) 63 | } 64 | hostInfo.AvgStat = loadInfo 65 | return &hostInfo 66 | } 67 | 68 | func getCpuInfo() *model.CpuInfo { 69 | defer timeCost()() 70 | 71 | var cpuInfo model.CpuInfo 72 | 73 | ci, err := cpu.Info() 74 | if err != nil { 75 | println("cpu.Info error:", err) 76 | } else { 77 | cpuModelCount := make(map[string]int) 78 | for i := 0; i < len(ci); i++ { 79 | cpuModelCount[ci[i].ModelName]++ 80 | } 81 | for m, count := range cpuModelCount { 82 | cpuInfo.Info = append(cpuInfo.Info, fmt.Sprintf("%s x %d ", m, count)) 83 | } 84 | } 85 | cpuPercent, _ := cpu.Percent(0, false) 86 | cpuInfo.Percent = cpuPercent[0] 87 | return &cpuInfo 88 | } 89 | 90 | func getMemInfo() *model.VirtualMemoryInfo { 91 | defer timeCost()() 92 | 93 | memInfo, err := mem.VirtualMemory() 94 | if err != nil { 95 | fmt.Println("get memory info fail. err: ", err) 96 | } 97 | 98 | var memRet model.VirtualMemoryInfo 99 | memRet.Total = memInfo.Total 100 | //memRet.Available = memInfo.Available 101 | //memRet.Free = memInfo.Free 102 | memRet.Used = memInfo.Used 103 | memRet.UsedPercent = memInfo.UsedPercent 104 | return &memRet 105 | } 106 | 107 | func getSwapMemInfo() *model.SwapMemoryInfo { 108 | defer timeCost()() 109 | 110 | ms, err := mem.SwapMemory() 111 | if err != nil { 112 | println("mem.SwapMemory error:", err) 113 | } 114 | 115 | var swapInfo model.SwapMemoryInfo 116 | swapInfo.Total = ms.Total 117 | swapInfo.Free = ms.Free 118 | swapInfo.Used = ms.Used 119 | swapInfo.UsedPercent = ms.UsedPercent 120 | return &swapInfo 121 | } 122 | 123 | func getDiskInfo() *model.DiskInfo { 124 | defer timeCost()() 125 | 126 | diskPart, err := disk.Partitions(false) 127 | if err != nil { 128 | fmt.Println(err) 129 | } 130 | var diskInfo model.DiskInfo 131 | var total, used uint64 132 | 133 | for _, dp := range diskPart { 134 | diskUsed, _ := disk.Usage(dp.Mountpoint) 135 | //fmt.Printf("%s %d %f %d \n", usage.Path, usage.Total, usage.UsedPercent, usage.Used) 136 | fsType := strings.ToLower(dp.Fstype) 137 | // 不统计 K8s 的虚拟挂载点:https://github.com/shirou/gopsutil/issues/1007 138 | if isListContainsStr(expectDiskFsTypes, fsType) && !strings.Contains(dp.Mountpoint, "/var/lib/kubelet") { 139 | p := model.Partition{ 140 | MountPoint: dp.Mountpoint, Fstype: dp.Fstype, 141 | Total: diskUsed.Total, Free: diskUsed.Free, 142 | Used: diskUsed.Used, UsedPercent: diskUsed.UsedPercent, 143 | } 144 | diskInfo.Partitions = append(diskInfo.Partitions, &p) 145 | total += diskUsed.Total 146 | used += diskUsed.Used 147 | } 148 | } 149 | diskInfo.Used = used 150 | diskInfo.Total = total 151 | //计算占用百分比 152 | diskInfo.UsedPercent = float64(used) / float64(total) * 100 153 | return &diskInfo 154 | } 155 | 156 | // StatNetworkSpeed 更新网络统计(线程安全) 157 | func StatNetworkSpeed() { 158 | defer timeCost()() 159 | 160 | if err := globalNetworkStats.Update(); err != nil { 161 | fmt.Println("更新网络统计失败: ", err) 162 | } 163 | } 164 | 165 | // 网络信息 参考 nezha(线程安全) 166 | func getNetInfo() *model.NetworkInfo { 167 | return globalNetworkStats.GetStats() 168 | } 169 | 170 | // FormatFileSize 字节的单位转换 保留两位小数 171 | // 导出此函数以供外部使用,避免 unused 警告 172 | func FormatFileSize(fileSize uint64) (size string) { 173 | if fileSize < 1024 { 174 | //return strconv.FormatInt(fileSize, 10) + "B" 175 | return fmt.Sprintf("%.2fB", float64(fileSize)/float64(1)) 176 | } else if fileSize < (1024 * 1024) { 177 | return fmt.Sprintf("%.2fKB", float64(fileSize)/float64(1024)) 178 | } else if fileSize < (1024 * 1024 * 1024) { 179 | return fmt.Sprintf("%.2fMB", float64(fileSize)/float64(1024*1024)) 180 | } else if fileSize < (1024 * 1024 * 1024 * 1024) { 181 | return fmt.Sprintf("%.2fGB", float64(fileSize)/float64(1024*1024*1024)) 182 | } else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) { 183 | return fmt.Sprintf("%.2fTB", float64(fileSize)/float64(1024*1024*1024*1024)) 184 | } else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024) 185 | return fmt.Sprintf("%.2fPB", float64(fileSize)/float64(1024*1024*1024*1024*1024)) 186 | } 187 | } 188 | 189 | var excludeNetInterfaces = []string{ 190 | "lo", "tun", "docker", "veth", "br-", "vmbr", "vnet", "kube", 191 | } 192 | var expectDiskFsTypes = []string{ 193 | "apfs", "ext4", "ext3", "ext2", "f2fs", "reiserfs", "jfs", "btrfs", 194 | "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs", "fuse.rclone", 195 | } 196 | 197 | // 全局网络统计收集器(线程安全) 198 | var globalNetworkStats = NewNetworkStatsCollector(excludeNetInterfaces) 199 | 200 | func isListContainsStr(list []string, str string) bool { 201 | for i := 0; i < len(list); i++ { 202 | if strings.Contains(str, list[i]) { 203 | return true 204 | } 205 | } 206 | return false 207 | } 208 | -------------------------------------------------------------------------------- /docs/development/contributing.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | > **作者**: ruan 4 | > **最后更新**: 2025-11-07 5 | 6 | ## 欢迎贡献 7 | 8 | 感谢你对 SimpleServerStatus 的关注!我们欢迎各种形式的贡献: 9 | 10 | - 🐛 报告 Bug 11 | - ✨ 提出新功能建议 12 | - 📝 改进文档 13 | - 💻 提交代码 14 | 15 | ## 行为准则 16 | 17 | 为了营造开放友好的环境,我们承诺: 18 | 19 | - ✅ 使用友好和包容的语言 20 | - ✅ 尊重不同的观点和经验 21 | - ✅ 优雅地接受建设性批评 22 | - ❌ 禁止人身攻击、骚扰或不专业的行为 23 | 24 | ## 开始之前 25 | 26 | ### 环境搭建 27 | 28 | 请先阅读 [开发环境搭建](./setup.md) 文档,确保开发环境已正确配置。 29 | 30 | ### 了解项目 31 | 32 | 建议先阅读以下文档: 33 | 34 | - [架构概览](../architecture/overview.md) - 了解系统架构 35 | - [WebSocket 通信设计](../architecture/websocket.md) - 了解通信机制 36 | - [数据流向](../architecture/data-flow.md) - 了解数据流转 37 | 38 | ## 报告 Bug 39 | 40 | ### 提交前检查 41 | 42 | 1. **搜索已有 Issue** - 检查问题是否已被报告 43 | 2. **使用最新版本** - 确认问题在最新版本中仍存在 44 | 3. **准备详细信息** - 收集必要的调试信息 45 | 46 | ### Bug 报告模板 47 | 48 | ```markdown 49 | **Bug 描述** 50 | 简洁清晰地描述 bug 51 | 52 | **复现步骤** 53 | 1. 启动 '...' 54 | 2. 配置 '...' 55 | 3. 执行 '...' 56 | 4. 看到错误 57 | 58 | **预期行为 vs 实际行为** 59 | - 预期: [描述期望] 60 | - 实际: [描述实际] 61 | 62 | **环境信息** 63 | - 操作系统: [如 Ubuntu 22.04] 64 | - Go 版本: [如 1.23.2] 65 | - 项目版本: [如 v1.0.0] 66 | 67 | **日志输出** 68 | ```log 69 | 粘贴相关日志 70 | ``` 71 | ``` 72 | 73 | ## 提出功能建议 74 | 75 | ```markdown 76 | **功能描述** 77 | 清晰描述希望实现的功能 78 | 79 | **使用场景** 80 | 描述这个功能解决什么问题 81 | 82 | **优先级** 83 | - [ ] 高(核心功能缺失) 84 | - [ ] 中(重要改进) 85 | - [ ] 低(可有可无) 86 | ``` 87 | 88 | ## 代码贡献流程 89 | 90 | ### 1. Fork 项目 91 | 92 | ```bash 93 | # 1. 在 GitHub 上点击 Fork 按钮 94 | 95 | # 2. 克隆你的 fork 96 | git clone https://github.com/YOUR_USERNAME/simple-server-status.git 97 | cd simple-server-status 98 | 99 | # 3. 添加上游仓库 100 | git remote add upstream https://github.com/ruanun/simple-server-status.git 101 | ``` 102 | 103 | ### 2. 创建分支 104 | 105 | ```bash 106 | # 更新 master 分支 107 | git checkout master 108 | git pull upstream master 109 | 110 | # 创建功能分支(使用描述性名称) 111 | git checkout -b feature/add-memory-alerts 112 | ``` 113 | 114 | **分支命名规范**: 115 | 116 | - `feature/` - 新功能 117 | - `fix/` - Bug 修复 118 | - `docs/` - 文档改进 119 | - `refactor/` - 代码重构 120 | - `test/` - 测试相关 121 | 122 | ### 3. 编写代码 123 | 124 | #### 代码规范 125 | 126 | **Go 代码**: 127 | 128 | ```bash 129 | # 运行 golangci-lint 130 | golangci-lint run 131 | 132 | # 格式化代码 133 | go fmt ./... 134 | 135 | # 整理依赖 136 | go mod tidy 137 | ``` 138 | 139 | **TypeScript 代码**: 140 | 141 | ```bash 142 | cd web 143 | pnpm run type-check # 类型检查 144 | pnpm run lint # 代码检查 145 | ``` 146 | 147 | #### 编码最佳实践 148 | 149 | **Go**: 150 | 151 | - ✅ 使用有意义的变量名 152 | - ✅ 处理所有错误(不要忽略) 153 | - ✅ 使用 `context.Context` 支持取消 154 | - ✅ 避免全局变量,使用依赖注入 155 | - ✅ 并发访问使用 mutex 保护 156 | - ✅ defer 关闭资源 157 | 158 | **TypeScript**: 159 | 160 | - ✅ 使用 TypeScript 类型注解 161 | - ✅ 避免使用 `any` 类型 162 | - ✅ 使用 Composition API 163 | - ✅ 组件职责单一 164 | 165 | ### 4. 编写测试 166 | 167 | ```go 168 | // 单元测试示例 169 | func TestNetworkStatsCollector_Concurrent(t *testing.T) { 170 | nsc := NewNetworkStatsCollector() 171 | 172 | var wg sync.WaitGroup 173 | wg.Add(2) 174 | 175 | go func() { 176 | defer wg.Done() 177 | for i := 0; i < 1000; i++ { 178 | nsc.Update(uint64(i), uint64(i*2)) 179 | } 180 | }() 181 | 182 | go func() { 183 | defer wg.Done() 184 | for i := 0; i < 1000; i++ { 185 | _, _ = nsc.GetStats() 186 | } 187 | }() 188 | 189 | wg.Wait() 190 | } 191 | ``` 192 | 193 | 运行测试: 194 | 195 | ```bash 196 | go test ./... # 运行所有测试 197 | go test -v ./... # 详细输出 198 | go test -cover ./... # 测试覆盖率 199 | go test -race ./... # 竞态检测 200 | ``` 201 | 202 | ### 5. 提交代码 203 | 204 | #### Commit Message 规范 205 | 206 | 使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: 207 | 208 | ``` 209 | (): 210 | ``` 211 | 212 | **Type(必填)**: 213 | 214 | - `feat` - 新功能 215 | - `fix` - Bug 修复 216 | - `docs` - 文档变更 217 | - `style` - 代码格式 218 | - `refactor` - 重构 219 | - `perf` - 性能优化 220 | - `test` - 测试相关 221 | - `chore` - 构建或工具变动 222 | 223 | **Scope(可选)**: `agent`, `dashboard`, `web`, `shared`, `docs` 224 | 225 | **示例**: 226 | 227 | ```bash 228 | # 好的提交消息 229 | git commit -m "feat(agent): 添加内存告警功能" 230 | git commit -m "fix(dashboard): 修复 WebSocket 断线重连问题" 231 | git commit -m "docs: 更新安装文档" 232 | 233 | # 不好的提交消息 234 | git commit -m "update" 235 | git commit -m "fix bug" 236 | ``` 237 | 238 | #### 提交步骤 239 | 240 | ```bash 241 | # 查看修改 242 | git status 243 | 244 | # 添加文件 245 | git add . 246 | 247 | # 提交 248 | git commit -m "feat(agent): 添加内存告警功能" 249 | 250 | # 推送到你的 fork 251 | git push origin feature/add-memory-alerts 252 | ``` 253 | 254 | ### 6. 创建 Pull Request 255 | 256 | #### PR 描述模板 257 | 258 | ```markdown 259 | ## 变更说明 260 | 261 | 简要描述这个 PR 做了什么 262 | 263 | ## 变更类型 264 | 265 | - [ ] 新功能 (feature) 266 | - [ ] Bug 修复 (fix) 267 | - [ ] 文档更新 (docs) 268 | - [ ] 代码重构 (refactor) 269 | 270 | ## 相关 Issue 271 | 272 | Closes #123 273 | 274 | ## 测试 275 | 276 | - [ ] 单元测试通过 277 | - [ ] 手动测试通过 278 | - [ ] 文档已更新 279 | 280 | ## 检查清单 281 | 282 | - [ ] 代码遵循项目规范 283 | - [ ] 已进行自我审查 284 | - [ ] 已添加必要注释 285 | - [ ] 已更新相关文档 286 | - [ ] 已添加测试 287 | - [ ] 新旧测试都通过 288 | ``` 289 | 290 | ### 7. 代码审查 291 | 292 | 1. 维护者会审查你的代码 293 | 2. 可能会提出修改建议 294 | 3. 根据建议进行修改 295 | 4. 推送更新到同一分支(PR 会自动更新) 296 | 297 | ```bash 298 | # 根据反馈修改代码并提交 299 | git add . 300 | git commit -m "refactor: 根据审查意见优化逻辑" 301 | git push origin feature/add-memory-alerts 302 | ``` 303 | 304 | ### 8. 合并后清理 305 | 306 | ```bash 307 | # PR 合并后,更新 master 分支 308 | git checkout master 309 | git pull upstream master 310 | 311 | # 删除功能分支 312 | git branch -d feature/add-memory-alerts 313 | git push origin --delete feature/add-memory-alerts 314 | ``` 315 | 316 | ## 文档贡献 317 | 318 | ### 文档规范 319 | 320 | **Markdown 格式**: 321 | 322 | - ✅ 使用标准 Markdown 语法 323 | - ✅ 代码块指定语言(```go, ```bash) 324 | - ✅ 使用有意义的标题层级 325 | 326 | **内容要求**: 327 | 328 | - ✅ 清晰准确 329 | - ✅ 示例完整可运行 330 | - ✅ 链接有效 331 | 332 | ### 文档提交流程 333 | 334 | 与代码贡献流程相同: 335 | 336 | ```bash 337 | # 创建分支 338 | git checkout -b docs/improve-installation-guide 339 | 340 | # 修改文档后提交 341 | git add docs/ 342 | git commit -m "docs: 改进安装指南" 343 | git push origin docs/improve-installation-guide 344 | ``` 345 | 346 | ## 获取帮助 347 | 348 | 如果遇到问题: 349 | 350 | 1. 查阅 [文档](../README.md) 351 | 2. 搜索 [已有 Issue](https://github.com/ruanun/simple-server-status/issues) 352 | 3. 创建新的 Issue 353 | 354 | ## 致谢 355 | 356 | 感谢所有贡献者!你们的贡献让这个项目变得更好。 357 | 358 | ## 相关文档 359 | 360 | - [开发环境搭建](./setup.md) - 环境配置 361 | - [架构概览](../architecture/overview.md) - 了解系统架构 362 | 363 | --- 364 | 365 | **版本**: 2.0 366 | **作者**: ruan 367 | **最后更新**: 2025-11-07 368 | -------------------------------------------------------------------------------- /docs/architecture/overview.md: -------------------------------------------------------------------------------- 1 | # 架构概览 2 | 3 | > **作者**: ruan 4 | > **最后更新**: 2025-11-05 5 | 6 | ## 项目概述 7 | 8 | SimpleServerStatus 是一个基于 Golang + Vue 的分布式服务器监控系统,采用 **Monorepo** 单仓库架构设计,实现了前后端分离、模块解耦、代码共享的现代化架构。 9 | 10 | ## 核心架构 11 | 12 | ### 系统组成 13 | 14 | ``` 15 | ┌────────────┐ ┌─────────────┐ ┌──────────┐ 16 | │ Agent │ WebSocket │ Dashboard │ WebSocket │ Web │ 17 | │ (采集端) │ ────────────► │ (服务端) │ ────────────► │ (展示端) │ 18 | └────────────┘ /ws-report └─────────────┘ /ws-frontend └──────────┘ 19 | ``` 20 | 21 | - **Agent**: 部署在被监控服务器上的监控代理,负责收集系统指标并通过 WebSocket 上报 22 | - **Dashboard**: 后端服务,管理 WebSocket 连接、提供 REST API 和静态资源服务 23 | - **Web**: Vue 3 前端用户界面,实时展示监控数据 24 | 25 | ### Monorepo 架构 26 | 27 | 项目采用 Monorepo 单仓库架构,统一管理所有模块: 28 | 29 | ``` 30 | simple-server-status/ 31 | ├── go.mod # 统一的 Go 模块定义 32 | │ 33 | ├── cmd/ # 程序入口 34 | │ ├── agent/main.go # Agent 启动入口 35 | │ └── dashboard/main.go # Dashboard 启动入口 36 | │ 37 | ├── pkg/ # 公共包(可被外部引用) 38 | │ └── model/ # 共享数据模型 39 | │ ├── server.go # 服务器信息 40 | │ ├── cpu.go # CPU 信息 41 | │ ├── memory.go # 内存信息 42 | │ ├── disk.go # 磁盘信息 43 | │ └── network.go # 网络信息 44 | │ 45 | ├── internal/ # 内部包(项目内部使用) 46 | │ ├── agent/ # Agent 实现 47 | │ ├── dashboard/ # Dashboard 实现 48 | │ └── shared/ # 共享基础设施 49 | │ ├── logging/ # 统一日志 50 | │ ├── config/ # 统一配置加载 51 | │ └── errors/ # 统一错误处理 52 | │ 53 | ├── configs/ # 配置文件示例 54 | ├── deployments/ # 部署配置 55 | ├── scripts/ # 构建和部署脚本 56 | └── web/ # Vue 3 前端 57 | ``` 58 | 59 | ### 架构特点 60 | 61 | ✅ **Monorepo 架构优势** 62 | - 统一 go.mod,避免版本冲突 63 | - 代码共享简单,直接 import 64 | - IDE 自动识别,代码跳转无障碍 65 | - 统一构建脚本和 CI/CD 66 | - 保持独立部署能力 67 | 68 | ✅ **标准项目布局** 69 | - 符合 Go 标准项目结构 70 | - `cmd/` 存放程序入口 71 | - `pkg/` 存放可导出的公共包 72 | - `internal/` 存放内部实现 73 | - 清晰的模块边界 74 | 75 | ✅ **依赖注入设计** 76 | - 基础设施包支持依赖注入 77 | - 减少全局变量使用 78 | - 提高代码可测试性 79 | - 便于单元测试和集成测试 80 | 81 | ## 数据模型 82 | 83 | ### 共享数据模型 (pkg/model) 84 | 85 | 所有数据模型定义在 `pkg/model/` 包中,Agent 和 Dashboard 共用: 86 | 87 | - **ServerInfo**: 服务器基本信息(ID、名称、分组、国家等) 88 | - **CPUInfo**: CPU 使用率、核心数等 89 | - **MemoryInfo**: 内存使用情况(总量、已用、可用等) 90 | - **DiskInfo**: 磁盘使用情况(分区、容量、读写速度) 91 | - **NetworkInfo**: 网络流量统计(上传、下载、速度) 92 | 93 | ### 导入路径规范 94 | 95 | ```go 96 | // 共享数据模型 97 | import "github.com/ruanun/simple-server-status/pkg/model" 98 | 99 | // Agent 内部包 100 | import "github.com/ruanun/simple-server-status/internal/agent/config" 101 | 102 | // Dashboard 内部包 103 | import "github.com/ruanun/simple-server-status/internal/dashboard/handler" 104 | 105 | // 共享基础设施 106 | import "github.com/ruanun/simple-server-status/internal/shared/logging" 107 | ``` 108 | 109 | ## 核心模块 110 | 111 | ### Agent 模块 112 | 113 | **职责**: 系统信息采集和上报 114 | 115 | **核心组件**: 116 | - **采集器 (gopsutil.go)**: 使用 gopsutil 库采集系统信息 117 | - **网络统计 (network_stats.go)**: 并发安全的网络流量统计 118 | - **数据上报 (report.go)**: 定时采集并通过 WebSocket 上报 119 | - **WebSocket 客户端 (ws.go)**: 维护与 Dashboard 的 WebSocket 连接 120 | - **性能监控 (monitor.go)**: 监控 Agent 自身性能 121 | - **内存池 (mempool.go)**: 优化内存分配,减少 GC 压力 122 | - **自适应采集 (adaptive.go)**: 根据系统负载动态调整采集频率 123 | 124 | **关键特性**: 125 | - ✅ 指数退避重连机制 126 | - ✅ 心跳保持连接 127 | - ✅ 并发安全的网络统计 128 | - ✅ Goroutine 优雅退出(Context 取消) 129 | - ✅ Channel 安全关闭 130 | - ✅ 内存池优化 131 | 132 | ### Dashboard 模块 133 | 134 | **职责**: WebSocket 连接管理、数据分发、Web 界面服务 135 | 136 | **核心组件**: 137 | - **WebSocket 管理器 (websocket_manager.go)**: 管理 Agent 连接 138 | - **前端 WebSocket 管理器 (frontend_websocket_manager.go)**: 管理前端连接 139 | - **HTTP 处理器 (handler/)**: REST API 处理 140 | - **中间件 (middleware.go)**: CORS、Recovery、日志等 141 | - **服务器初始化 (server/server.go)**: Gin 服务器初始化 142 | - **静态资源 (public/resource.go)**: 嵌入前端静态文件 143 | 144 | **关键特性**: 145 | - ✅ 双通道 WebSocket 设计(Agent 通道 + 前端通道) 146 | - ✅ 连接状态跟踪 147 | - ✅ 心跳超时检测 148 | - ✅ 并发连接管理 149 | - ✅ 静态文件嵌入部署 150 | 151 | ### 共享基础设施 (internal/shared) 152 | 153 | **日志模块 (logging/)**: 154 | - 基于 Zap 实现的结构化日志 155 | - 支持日志级别、文件输出、日志轮转 156 | - 统一的日志初始化接口 157 | 158 | **配置模块 (config/)**: 159 | - 基于 Viper 实现的配置加载 160 | - 支持多路径搜索 161 | - 支持环境变量覆盖 162 | - 支持配置热加载 163 | 164 | **错误处理模块 (errors/)**: 165 | - 统一的错误类型定义 166 | - 错误严重等级分类 167 | - 错误统计和历史记录 168 | - 重试机制(指数退避) 169 | 170 | ## 技术栈 171 | 172 | ### 后端技术 173 | 174 | - **Go 1.23.2**: 主要开发语言 175 | - **Gin 1.x**: HTTP 框架 176 | - **Melody**: WebSocket 库(Agent 连接) 177 | - **gorilla/websocket**: WebSocket 库(前端连接) 178 | - **gopsutil**: 系统信息采集 179 | - **Viper**: 配置管理 180 | - **Zap**: 结构化日志 181 | 182 | ### 前端技术 183 | 184 | - **Vue 3.5+**: 使用 Composition API 185 | - **TypeScript 5.6+**: 类型安全 186 | - **Ant Design Vue 4.x**: UI 组件库 187 | - **Vite 6.x**: 构建工具 188 | - **unplugin-vue-components**: 组件自动导入 189 | 190 | ## 编译和部署 191 | 192 | ### 独立编译 193 | 194 | ```bash 195 | # 编译 Agent(输出独立二进制) 196 | go build -o bin/sss-agent ./cmd/agent 197 | 198 | # 编译 Dashboard(输出独立二进制) 199 | go build -o bin/sss-dashboard ./cmd/dashboard 200 | ``` 201 | 202 | ### 多平台构建 203 | 204 | ```bash 205 | # 使用 goreleaser 构建多平台版本 206 | goreleaser release --snapshot --clean 207 | 208 | # 支持的平台 209 | # - Linux (amd64, arm, arm64) 210 | # - Windows (amd64) 211 | # - macOS (amd64, arm64) 212 | # - FreeBSD (amd64, arm64) 213 | ``` 214 | 215 | ### 部署方式 216 | 217 | **Agent 服务器**只需要: 218 | - `sss-agent` 二进制文件 219 | - `configs/sss-agent.yaml` 配置文件 220 | 221 | **Dashboard 服务器**只需要: 222 | - `sss-dashboard` 二进制文件 223 | - `configs/sss-dashboard.yaml` 配置文件 224 | - 前端静态文件(已嵌入到二进制中) 225 | 226 | ## 架构优势 227 | 228 | ### 对比传统架构 229 | 230 | **优化前(Go Workspace)**: 231 | ``` 232 | Agent ─依赖→ Dashboard/pkg/model ❌ 233 | 独立 go.mod × 2 + go.work ⚠️ 234 | 内部目录扁平化 ⚠️ 235 | 基础设施代码重复 ~330行 ⚠️ 236 | ``` 237 | 238 | **优化后(Monorepo)**: 239 | ``` 240 | Agent ←─共享─→ pkg/model ←─共享─→ Dashboard ✅ 241 | 统一 go.mod ✅ 242 | 清晰分层架构(cmd/pkg/internal) ✅ 243 | 共享基础设施(logging/config/errors) ✅ 244 | ``` 245 | 246 | ### 量化指标 247 | 248 | | 指标 | 优化前 | 优化后 | 提升 | 249 | |------|--------|--------|------| 250 | | **模块独立性** | ❌ Agent 依赖 Dashboard | ✅ 完全独立 | 100% | 251 | | **go.mod 文件** | 3 个(含 go.work) | 1 个 | -67% | 252 | | **代码重复** | ~330 行 | <50 行 | 85% | 253 | | **全局变量** | 10+ 个 | 支持依赖注入 | 显著改善 | 254 | | **目录层级** | 扁平化 | 清晰分层 | 200% | 255 | | **并发安全** | ❌ 存在竞态 | ✅ 完全安全 | 100% | 256 | 257 | ## 开发体验改进 258 | 259 | 1. **更快的编译**: Monorepo 单仓库,只编译修改部分 260 | 2. **更好的可维护性**: 清晰的分层和职责划分 261 | 3. **更容易扩展**: 接口抽象,便于添加功能 262 | 4. **更规范的结构**: 符合 Go 社区标准 263 | 5. **更安全的代码**: 修复了所有已知的并发安全问题 264 | 265 | ## 相关文档 266 | 267 | - [WebSocket 通信设计](./websocket.md) - WebSocket 双通道设计详解 268 | - [数据流向](./data-flow.md) - 系统数据流转过程 269 | - [开发指南](../development/setup.md) - 本地开发环境搭建 270 | - [API 文档](../api/rest-api.md) - REST API 接口说明 271 | 272 | --- 273 | 274 | **版本**: 1.0 275 | **作者**: ruan 276 | **最后更新**: 2025-11-05 277 | --------------------------------------------------------------------------------