├── akile_monitor.jpg ├── akile-monitor-cover.jpg ├── .dockerignore ├── client.json ├── config.json ├── client ├── config.go ├── model │ └── model.go ├── main.go └── monitor.go ├── config.go ├── docker-compose.yml ├── README.md ├── go.mod ├── Dockerfile ├── setup-monitor.sh ├── ak-setup.sh ├── .github └── workflows │ └── auto_build_docker_container.yml ├── setup-monitor-fe.sh ├── ak-update.sh ├── DOCKER.md ├── setup-client.sh ├── alpine-client.sh ├── main.go ├── tgbot.go ├── LICENSE └── go.sum /akile_monitor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akile-network/akile_monitor/HEAD/akile_monitor.jpg -------------------------------------------------------------------------------- /akile-monitor-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akile-network/akile_monitor/HEAD/akile-monitor-cover.jpg -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | Dockerfile 5 | .gitlab-ci.yml 6 | LICENSE 7 | README.md 8 | DOCKER.md 9 | -------------------------------------------------------------------------------- /client.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth_secret": "auth_secret", 3 | "url": "ws://localhost:3000/monitor", 4 | "net_name": "eth0", 5 | "name": "HK-Akile" 6 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "auth_secret": "auth_secret", 3 | "listen": ":3000", 4 | "enable_tg": false, 5 | "tg_token": "your_telegram_bot_token", 6 | "hook_uri": "/hook", 7 | "update_uri": "/monitor", 8 | "web_uri": "/ws", 9 | "hook_token": "hook_token", 10 | "tg_chat_id": 0 11 | } -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type Config struct { 10 | AuthSecret string `json:"auth_secret"` 11 | Url string `json:"url"` 12 | NetName string `json:"net_name"` 13 | Name string `json:"name"` 14 | } 15 | 16 | var cfg *Config 17 | 18 | func LoadConfig() { 19 | file, err := os.ReadFile("client.json") 20 | if err != nil { 21 | log.Panic(err) 22 | } 23 | cfg = &Config{} 24 | err = json.Unmarshal(file, cfg) 25 | if err != nil { 26 | log.Panic(err) 27 | } 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /client/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type HostState struct { 4 | CPU float64 5 | MemUsed uint64 6 | SwapUsed uint64 7 | NetInTransfer uint64 8 | NetOutTransfer uint64 9 | NetInSpeed uint64 10 | NetOutSpeed uint64 11 | Uptime uint64 12 | Load1 float64 13 | Load5 float64 14 | Load15 float64 15 | } 16 | 17 | type Host struct { 18 | Name string 19 | Platform string 20 | PlatformVersion string 21 | CPU []string 22 | MemTotal uint64 23 | SwapTotal uint64 24 | Arch string 25 | Virtualization string 26 | BootTime uint64 27 | } 28 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type Config struct { 10 | AuthSecret string `json:"auth_secret"` 11 | Listen string `json:"listen"` 12 | EnableTG bool `json:"enable_tg"` 13 | TgToken string `json:"tg_token"` 14 | UpdateUri string `json:"update_uri"` 15 | WebUri string `json:"web_uri"` 16 | HookUri string `json:"hook_uri"` 17 | HookToken string `json:"hook_token"` 18 | TgChatID int64 `json:"tg_chat_id"` 19 | } 20 | 21 | var cfg *Config 22 | 23 | func LoadConfig() { 24 | file, err := os.ReadFile("config.json") 25 | if err != nil { 26 | log.Panic(err) 27 | } 28 | cfg = &Config{} 29 | err = json.Unmarshal(file, cfg) 30 | if err != nil { 31 | log.Panic(err) 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | akile_monitor_server: 4 | image: niliaerith/akile_monitor_server 5 | container_name: akile_monitor_server 6 | hostname: akile_monitor_server 7 | restart: always 8 | ports: 9 | - 3000:3000 #主控服务端 端口 10 | volumes: 11 | - /CHANGE_PATH/akile_monitor/server/ak_monitor.db:/app/ak_monitor.db 12 | environment: 13 | TZ: "Asia/Shanghai" 14 | AUTH_SECRET: "auth_secret" 15 | LISTEN: ":3000" 16 | ENABLE_TG: false 17 | TG_TOKEN: "your_telegram_bot_token" 18 | HOOK_URI: "/hook" 19 | UPDATE_URI: "/monitor" 20 | WEB_URI: "/ws" 21 | HOOK_TOKEN: "hook_token" 22 | TG_CHAT_ID: 0 23 | 24 | akile_monitor_fe: 25 | image: niliaerith/akile_monitor_fe 26 | container_name: akile_monitor_fe 27 | hostname: akile_monitor_fe 28 | restart: always 29 | ports: 30 | - 80:80 #前端 端口 31 | environment: 32 | TZ: "Asia/Shanghai" 33 | SOCKET: "ws://192.168.31.64:3000/ws" 34 | APIURL: "http://192.168.31.64:3000" 35 | 36 | akile_monitor_client: 37 | image: niliaerith/akile_monitor_client 38 | container_name: akile_monitor_client 39 | hostname: akile_monitor_client 40 | restart: always 41 | network_mode: host 42 | volumes: 43 | - /var/run/docker.sock:/var/run/docker.sock 44 | environment: 45 | TZ: "Asia/Shanghai" 46 | AUTH_SECRET: "auth_secret" 47 | URL: "ws://localhost:3000/monitor" 48 | NET_NAME: "eth0" 49 | NAME: "HK-Akile" 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akile Monitor 2 | 3 | ![预览图](https://github.com/akile-network/akile_monitor/blob/main/akile-monitor-cover.jpg?raw=true) 4 | Demo https://cpu.icu 5 | 6 | 7 | 前端项目地址 https://github.com/akile-network/akile_monitor_fe 8 | 9 | ## [Docker部署](./DOCKER.md) 10 | 11 | ## 前后端集合一键脚本 12 | 13 | ``` 14 | wget -O ak-setup.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/ak-setup.sh" && chmod +x ak-setup.sh && sudo ./ak-setup.sh 15 | ``` 16 | ![image](https://github.com/user-attachments/assets/58b9209b-a327-4783-b9dd-4e0dc2ecbf7e) 17 | 18 | ## 主控后端 19 | 20 | ``` 21 | wget -O setup-monitor.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/setup-monitor.sh" && chmod +x setup-monitor.sh && sudo ./setup-monitor.sh 22 | ``` 23 | 24 | ## 被控端 25 | 26 | ``` 27 | wget -O setup-client.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/setup-client.sh" && chmod +x setup-client.sh && sudo ./setup-client.sh 28 | ``` 29 | 如 30 | ``` 31 | wget -O setup-client.sh "https://raw.githubusercontent.com/akile-network/akile_monitor/refs/heads/main/setup-client.sh" && chmod +x setup-client.sh && sudo ./setup-client.sh 123321 wss://123.321.123.321/monitor HKLite-One-Akile 32 | ``` 33 | 34 | ## 主控前端部署教程(cf pages) 35 | 36 | ### 1.下载 37 | 38 | https://github.com/akile-network/akile_monitor_fe/releases/latest/download/akile_monitor_fe.zip 39 | 40 | 41 | ### 2.修改config.json为自己的api地址(公网地址)(如果前端要加ssl 后端也要加ssl 且此处记得改为https和wss) 42 | 43 | ``` 44 | { 45 | "socket": "ws(s)://192.168.31.64:3000/ws", 46 | "apiURL": "http(s)://192.168.31.64:3000" 47 | } 48 | ``` 49 | 50 | ### 3.直接上传文件夹至cf pages 51 | 52 | ![image](https://github.com/user-attachments/assets/c9e5a950-045a-4a7f-8b30-00899994c8cf) 53 | ![image](https://github.com/user-attachments/assets/c4096133-694d-4c2a-8d90-f92e48de6e9b) 54 | 55 | ### 4.设置域名(可选) 56 | 57 | ![image](https://github.com/user-attachments/assets/14adc0cf-2292-4148-a913-7a466e441d71) 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module akile_monitor 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/cloudwego/hertz v0.9.3 7 | github.com/glebarez/sqlite v1.11.0 8 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 9 | github.com/gorilla/websocket v1.5.3 10 | github.com/henrylee2cn/goutil v1.0.1 11 | github.com/hertz-contrib/cors v0.1.0 12 | github.com/hertz-contrib/websocket v0.1.0 13 | github.com/shirou/gopsutil/v3 v3.24.5 14 | gorm.io/gorm v1.25.12 15 | ) 16 | 17 | require ( 18 | github.com/andeya/goutil v1.0.1 // indirect 19 | github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect 20 | github.com/bytedance/gopkg v0.1.0 // indirect 21 | github.com/bytedance/sonic v1.12.0 // indirect 22 | github.com/bytedance/sonic/loader v0.2.0 // indirect 23 | github.com/cloudwego/base64x v0.1.4 // indirect 24 | github.com/cloudwego/iasm v0.2.0 // indirect 25 | github.com/cloudwego/netpoll v0.6.2 // indirect 26 | github.com/dustin/go-humanize v1.0.1 // indirect 27 | github.com/fsnotify/fsnotify v1.5.4 // indirect 28 | github.com/glebarez/go-sqlite v1.21.2 // indirect 29 | github.com/go-ole/go-ole v1.2.6 // indirect 30 | github.com/golang/protobuf v1.5.0 // indirect 31 | github.com/google/uuid v1.3.0 // indirect 32 | github.com/henrylee2cn/ameda v1.4.10 // indirect 33 | github.com/jinzhu/inflection v1.0.0 // indirect 34 | github.com/jinzhu/now v1.1.5 // indirect 35 | github.com/klauspost/cpuid/v2 v2.2.3 // indirect 36 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 37 | github.com/mattn/go-isatty v0.0.17 // indirect 38 | github.com/nyaruka/phonenumbers v1.0.55 // indirect 39 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 40 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 41 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 42 | github.com/tidwall/gjson v1.14.4 // indirect 43 | github.com/tidwall/match v1.1.1 // indirect 44 | github.com/tidwall/pretty v1.2.0 // indirect 45 | github.com/tklauser/go-sysconf v0.3.12 // indirect 46 | github.com/tklauser/numcpus v0.6.1 // indirect 47 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 48 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 49 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 50 | golang.org/x/sys v0.24.0 // indirect 51 | golang.org/x/text v0.14.0 // indirect 52 | google.golang.org/protobuf v1.27.1 // indirect 53 | modernc.org/libc v1.22.5 // indirect 54 | modernc.org/mathutil v1.5.0 // indirect 55 | modernc.org/memory v1.5.0 // indirect 56 | modernc.org/sqlite v1.23.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "akile_monitor/client/model" 5 | "bytes" 6 | "compress/gzip" 7 | "flag" 8 | "github.com/cloudwego/hertz/pkg/common/json" 9 | "github.com/henrylee2cn/goutil/calendar/cron" 10 | "log" 11 | "os" 12 | "os/signal" 13 | "time" 14 | 15 | "github.com/gorilla/websocket" 16 | ) 17 | 18 | func main() { 19 | LoadConfig() 20 | 21 | go func() { 22 | c := cron.New() 23 | c.AddFunc("* * * * * *", func() { 24 | TrackNetworkSpeed() 25 | }) 26 | c.Start() 27 | }() 28 | 29 | flag.Parse() 30 | log.SetFlags(0) 31 | 32 | interrupt := make(chan os.Signal, 1) 33 | signal.Notify(interrupt, os.Interrupt) 34 | 35 | u := cfg.Url 36 | log.Printf("connecting to %s", u) 37 | 38 | c, _, err := websocket.DefaultDialer.Dial(cfg.Url, nil) 39 | if err != nil { 40 | log.Fatal("dial:", err) 41 | } 42 | defer c.Close() 43 | 44 | c.WriteMessage(websocket.TextMessage, []byte(cfg.AuthSecret)) 45 | 46 | done := make(chan struct{}) 47 | 48 | _, message, err := c.ReadMessage() 49 | if err != nil { 50 | log.Println("auth_secret验证失败") 51 | log.Println("read:", err) 52 | return 53 | } 54 | if string(message) == "auth success" { 55 | log.Println("auth_secret验证成功") 56 | log.Println("正在上报数据...") 57 | } 58 | 59 | ticker := time.NewTicker(time.Second) 60 | defer ticker.Stop() 61 | 62 | for { 63 | select { 64 | case <-done: 65 | return 66 | case t := <-ticker.C: 67 | var D struct { 68 | Host *model.Host 69 | State *model.HostState 70 | TimeStamp int64 71 | } 72 | D.Host = GetHost() 73 | D.State = GetState() 74 | D.TimeStamp = t.Unix() 75 | //gzip压缩json 76 | dataBytes, err := json.Marshal(D) 77 | if err != nil { 78 | log.Println("json.Marshal error:", err) 79 | return 80 | } 81 | 82 | var buf bytes.Buffer 83 | gz := gzip.NewWriter(&buf) 84 | if _, err := gz.Write(dataBytes); err != nil { 85 | log.Println("gzip.Write error:", err) 86 | return 87 | } 88 | 89 | if err := gz.Close(); err != nil { 90 | log.Println("gzip.Close error:", err) 91 | return 92 | } 93 | 94 | err = c.WriteMessage(websocket.TextMessage, buf.Bytes()) 95 | if err != nil { 96 | log.Println("write:", err) 97 | return 98 | } 99 | case <-interrupt: 100 | log.Println("interrupt") 101 | err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 102 | if err != nil { 103 | log.Println("write close:", err) 104 | return 105 | } 106 | select { 107 | case <-done: 108 | case <-time.After(time.Second): 109 | } 110 | return 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS gobuild 2 | WORKDIR /build 3 | COPY . /build 4 | RUN go mod download && \ 5 | go mod tidy && \ 6 | go mod verify && \ 7 | go build 8 | RUN cd client && \ 9 | go mod download && \ 10 | go mod tidy && \ 11 | go mod verify && \ 12 | go build 13 | 14 | FROM node:lts-alpine AS nodebuild 15 | WORKDIR /build 16 | RUN apk add git && \ 17 | git clone https://github.com/akile-network/akile_monitor_fe.git amf && \ 18 | cd amf && \ 19 | npm install && \ 20 | npm run build && \ 21 | rm -rf dist/config.json 22 | 23 | FROM alpine AS server 24 | WORKDIR /app 25 | 26 | ENV AUTH_SECRET=${AUTH_SECRET:-auth_secret} 27 | ENV LISTEN=${LISTEN:-:3000} 28 | ENV ENABLE_TG=${ENABLE_TG:-false} 29 | ENV TG_TOKEN=${TG_TOKEN:-your_telegram_bot_token} 30 | ENV HOOK_URI=${HOOK_URI:-/hook} 31 | ENV UPDATE_URI=${UPDATE_URI:-/monitor} 32 | ENV WEB_URI=${WEB_URI:-/ws} 33 | ENV HOOK_TOKEN=${HOOK_TOKEN:-hook_token} 34 | ENV TG_CHAT_ID=${TG_CHAT_ID:-0} 35 | 36 | COPY --from=gobuild /build/akile_monitor /app/ak_monitor 37 | 38 | RUN cat <<'EOF' > entrypoint.sh 39 | #!/bin/sh 40 | if [ ! -f "config.json" ]; then 41 | echo "{ 42 | \"auth_secret\": \"${AUTH_SECRET}\", 43 | \"listen\": \"${LISTEN}\", 44 | \"enable_tg\": ${ENABLE_TG}, 45 | \"tg_token\": \"${TG_TOKEN}\", 46 | \"hook_uri\": \"${HOOK_URI}\", 47 | \"update_uri\": \"${UPDATE_URI}\", 48 | \"web_uri\": \"${WEB_URI}\", 49 | \"hook_token\": \"${HOOK_TOKEN}\", 50 | \"tg_chat_id\": ${TG_CHAT_ID} 51 | }"> config.json 52 | fi 53 | /app/ak_monitor 54 | EOF 55 | 56 | EXPOSE 3000 57 | 58 | RUN chmod +x ak_monitor entrypoint.sh 59 | CMD ["./entrypoint.sh"] 60 | 61 | FROM caddy:latest AS fe 62 | WORKDIR /app 63 | 64 | ENV SOCKET=${SOCKET:-ws://192.168.31.64:3000/ws} 65 | ENV APIURL=${APIURL:-http://192.168.31.64:3000} 66 | 67 | COPY --from=nodebuild /build/amf/dist /usr/share/caddy 68 | 69 | RUN cat <<'EOF' > entrypoint.sh 70 | #!/bin/sh 71 | if [ ! -f "/usr/share/caddy/config.json" ]; then 72 | echo "{ 73 | \"socket\": \"${SOCKET}\", 74 | \"apiURL\": \"${APIURL}\" 75 | }"> /usr/share/caddy/config.json 76 | fi 77 | caddy run --config /etc/caddy/Caddyfile --adapter caddyfile 78 | EOF 79 | 80 | EXPOSE 80 81 | 82 | RUN chmod +x entrypoint.sh 83 | CMD ["./entrypoint.sh"] 84 | 85 | FROM alpine AS client 86 | WORKDIR /app 87 | 88 | ENV AUTH_SECRET=${AUTH_SECRET:-auth_secret} 89 | ENV URL=${URL:-ws://localhost:3000/monitor} 90 | ENV NET_NAME=${NET_NAME:-eth0} 91 | ENV NAME=${NAME:-HK-Akile} 92 | 93 | COPY --from=gobuild /build/client/client /app/ak_client 94 | 95 | RUN cat <<'EOF' > entrypoint.sh 96 | #!/bin/sh 97 | if [ ! -f "client.json" ]; then 98 | echo "{ 99 | \"auth_secret\": \"${AUTH_SECRET}\", 100 | \"url\": \"${URL}\", 101 | \"net_name\": \"${NET_NAME}\", 102 | \"name\": \"${NAME}\" 103 | }"> client.json 104 | fi 105 | /app/ak_client 106 | EOF 107 | 108 | RUN chmod +x ak_client entrypoint.sh 109 | CMD ["./entrypoint.sh"] -------------------------------------------------------------------------------- /setup-monitor.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if running as root 3 | if [ "$EUID" -ne 0 ]; then 4 | echo "Please run as root" 5 | exit 1 6 | fi 7 | 8 | # Stop existing service if running 9 | systemctl stop ak_monitor 10 | 11 | # Get system architecture 12 | ARCH=$(uname -m) 13 | MONITOR_FILE="akile_monitor-linux-amd64" 14 | 15 | # Set appropriate monitor file based on architecture 16 | if [ "$ARCH" = "x86_64" ]; then 17 | MONITOR_FILE="akile_monitor-linux-amd64" 18 | elif [ "$ARCH" = "aarch64" ]; then 19 | MONITOR_FILE="akile_monitor-linux-arm64" 20 | elif [ "$ARCH" = "x86_64" ] && [ "$(uname -s)" = "Darwin" ]; then 21 | MONITOR_FILE="akile_monitor-darwin-amd64" 22 | else 23 | echo "Unsupported architecture: $ARCH" 24 | exit 1 25 | fi 26 | 27 | # Create directory and change to it 28 | mkdir -p /etc/ak_monitor/ 29 | cd /etc/ak_monitor/ 30 | 31 | # Download monitor 32 | wget -O ak_monitor https://github.com/akile-network/akile_monitor/releases/latest/download/$MONITOR_FILE 33 | chmod 777 ak_monitor 34 | 35 | # Create service file 36 | cat > /etc/systemd/system/ak_monitor.service < /etc/ak_monitor/config.json < /dev/null; then 17 | # Debian/Ubuntu 18 | echo "检测到 Debian/Ubuntu 系统" 19 | apt-get update 20 | apt-get install -y wget unzip curl debian-keyring debian-archive-keyring apt-transport-https 21 | 22 | # 安装 Caddy 23 | if ! command -v caddy &> /dev/null; then 24 | echo "正在安装 Caddy..." 25 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/setup.deb.sh' | bash 26 | apt-get install caddy 27 | fi 28 | 29 | elif command -v yum &> /dev/null; then 30 | # CentOS/RHEL 31 | echo "检测到 CentOS/RHEL 系统" 32 | yum install -y wget unzip curl yum-utils 33 | 34 | # 安装 Caddy 35 | if ! command -v caddy &> /dev/null; then 36 | echo "正在安装 Caddy..." 37 | yum install -y 'dnf-command(copr)' 38 | yum copr enable -y @caddy/caddy 39 | yum install -y caddy 40 | fi 41 | else 42 | echo "不支持的操作系统" 43 | exit 1 44 | fi 45 | 46 | # 创建目录 47 | echo "创建安装目录..." 48 | mkdir -p /etc/ak_monitor/frontend 49 | cd /etc/ak_monitor/frontend 50 | 51 | # 下载并解压前端文件 52 | echo "正在下载前端包..." 53 | wget -O frontend.zip https://github.com/akile-network/akile_monitor_fe/releases/latest/download/akile_monitor_fe.zip 54 | echo "正在解压文件..." 55 | unzip -o frontend.zip 56 | rm frontend.zip 57 | 58 | # 获取用户输入 59 | read -p "请设置前端域名(已解析到此服务器的域名,例如:monitor.example.com): " frontend_domain 60 | read -p "请设置后端域名(已解析到此服务器的域名,例如:api.example.com): " backend_domain 61 | read -p "请输入后端服务端口(例如:3000): " backend_port 62 | 63 | # 验证端口号 64 | if ! [[ "$backend_port" =~ ^[0-9]+$ ]] || [ "$backend_port" -lt 1 ] || [ "$backend_port" -gt 65535 ]; then 65 | echo "错误:无效的端口号" 66 | exit 1 67 | fi 68 | 69 | # 创建前端配置文件 70 | echo "正在配置前端..." 71 | cat > /etc/ak_monitor/frontend/config.json < /etc/caddy/Caddyfile < /dev/null; then 105 | # CentOS/RHEL 防火墙配置 106 | firewall-cmd --permanent --zone=public --add-service=http 107 | firewall-cmd --permanent --zone=public --add-service=https 108 | firewall-cmd --reload 109 | elif command -v ufw &> /dev/null; then 110 | # Ubuntu/Debian 防火墙配置 111 | ufw allow 80/tcp 112 | ufw allow 443/tcp 113 | fi 114 | 115 | # 启动并启用 Caddy 服务 116 | echo "正在启动 Caddy 服务..." 117 | systemctl restart caddy 118 | systemctl enable caddy 119 | 120 | echo "安装完成!" 121 | echo "前端已部署到 https://${frontend_domain}" 122 | echo "后端已配置到 https://${backend_domain},反向代理到本地端口 ${backend_port}" 123 | echo "请确保:" 124 | echo "1. ${frontend_domain} 和 ${backend_domain} 的 DNS 记录均已正确解析到此服务器" 125 | echo "2. 后端服务正在端口 ${backend_port} 上运行" 126 | echo "Caddy 服务状态:" 127 | systemctl status caddy 128 | 129 | # 额外信息 130 | if [ "$OS" = "centos" ] || [ "$OS" = "rhel" ]; then 131 | echo -e "\nCentOS/RHEL 系统的重要提示:" 132 | echo "1. 如果启用了 SELinux,您可能需要运行以下命令:" 133 | echo " semanage fcontext -a -t httpd_sys_content_t \"/etc/ak_monitor/frontend(/.*)?\" " 134 | echo " restorecon -Rv /etc/ak_monitor/frontend" 135 | echo "2. 如果使用云服务器,请确保在安全组中开放 80 和 443 端口" 136 | fi 137 | -------------------------------------------------------------------------------- /client/monitor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "akile_monitor/client/model" 5 | "fmt" 6 | "github.com/shirou/gopsutil/v3/cpu" 7 | "github.com/shirou/gopsutil/v3/host" 8 | "github.com/shirou/gopsutil/v3/load" 9 | "github.com/shirou/gopsutil/v3/mem" 10 | "github.com/shirou/gopsutil/v3/net" 11 | "log" 12 | "runtime" 13 | "strconv" 14 | "time" 15 | "github.com/docker/docker/client" 16 | "context" 17 | ) 18 | 19 | func GetState() *model.HostState { 20 | var ret model.HostState 21 | cp, err := cpu.Percent(0, false) 22 | if err != nil || len(cp) == 0 { 23 | log.Println("cpu.Percent error:", err) 24 | } else { 25 | ret.CPU = cp[0] 26 | } 27 | 28 | loadStat, err := load.Avg() 29 | if err != nil { 30 | log.Println("load.Avg error:", err) 31 | } else { 32 | ret.Load1 = Decimal(loadStat.Load1) 33 | ret.Load5 = Decimal(loadStat.Load5) 34 | ret.Load15 = Decimal(loadStat.Load15) 35 | 36 | } 37 | 38 | vm, err := mem.VirtualMemory() 39 | if err != nil { 40 | log.Println("mem.VirtualMemory error:", err) 41 | } else { 42 | ret.MemUsed = vm.Total - vm.Available 43 | } 44 | 45 | uptime, err := host.Uptime() 46 | if err != nil { 47 | log.Println("host.Uptime error:", err) 48 | } else { 49 | ret.Uptime = uptime 50 | } 51 | 52 | swap, err := mem.SwapMemory() 53 | if err != nil { 54 | log.Println("mem.SwapMemory error:", err) 55 | } else { 56 | ret.SwapUsed = swap.Used 57 | } 58 | 59 | ret.NetInTransfer, ret.NetOutTransfer = netInTransfer, netOutTransfer 60 | ret.NetInSpeed, ret.NetOutSpeed = netInSpeed, netOutSpeed 61 | 62 | return &ret 63 | 64 | } 65 | 66 | func GetHost() *model.Host { 67 | var ret model.Host 68 | ret.Name = cfg.Name 69 | var cpuType string 70 | hi, err := host.Info() 71 | if err != nil { 72 | log.Println("host.Info error:", err) 73 | } 74 | cpuType = "Virtual" 75 | ret.Platform = hi.Platform 76 | ret.PlatformVersion = hi.PlatformVersion 77 | ret.Arch = hi.KernelArch 78 | ret.Virtualization = hi.VirtualizationSystem 79 | ret.BootTime = hi.BootTime 80 | 81 | // 检查是否在 Docker 环境中 82 | if ret.Virtualization == "docker" { 83 | // 创建 Docker 客户端 84 | cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 85 | if err != nil { 86 | log.Println("Failed to create Docker client:", err) 87 | } else { 88 | defer cli.Close() 89 | 90 | // 获取 Docker 信息 91 | info, err := cli.Info(context.Background()) 92 | if err != nil { 93 | log.Println("Failed to get Docker info:", err) 94 | } else { 95 | // 更新宿主机信息 96 | ret.Platform = info.OperatingSystem 97 | ret.PlatformVersion = "" 98 | ret.Arch = info.Architecture 99 | } 100 | } 101 | } 102 | 103 | ci, err := cpu.Info() 104 | if err != nil { 105 | log.Println("cpu.Info error:", err) 106 | } 107 | ret.CPU = append(ret.CPU, fmt.Sprintf("%s %d %s Core", ci[0].ModelName, runtime.NumCPU(), cpuType)) 108 | vm, err := mem.VirtualMemory() 109 | if err != nil { 110 | log.Println("mem.VirtualMemory error:", err) 111 | } 112 | 113 | swap, err := mem.SwapMemory() 114 | if err != nil { 115 | log.Println("mem.SwapMemory error:", err) 116 | } 117 | 118 | ret.MemTotal = vm.Total 119 | ret.SwapTotal = swap.Total 120 | return &ret 121 | 122 | } 123 | 124 | var ( 125 | netInSpeed, netOutSpeed, netInTransfer, netOutTransfer, lastUpdateNetStats uint64 126 | ) 127 | 128 | // TrackNetworkSpeed NIC监控,统计流量与速度 129 | func TrackNetworkSpeed() { 130 | var innerNetInTransfer, innerNetOutTransfer uint64 131 | nc, err := net.IOCounters(true) 132 | if err == nil { 133 | for _, v := range nc { 134 | if v.Name == cfg.NetName { 135 | innerNetInTransfer += v.BytesRecv 136 | innerNetOutTransfer += v.BytesSent 137 | } 138 | } 139 | now := uint64(time.Now().Unix()) 140 | diff := now - lastUpdateNetStats 141 | if diff > 0 { 142 | netInSpeed = (innerNetInTransfer - netInTransfer) / diff 143 | netOutSpeed = (innerNetOutTransfer - netOutTransfer) / diff 144 | } 145 | netInTransfer = innerNetInTransfer 146 | netOutTransfer = innerNetOutTransfer 147 | lastUpdateNetStats = now 148 | 149 | } 150 | } 151 | 152 | // 保留两位小数 153 | func Decimal(value float64) float64 { 154 | value, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", value), 64) 155 | return value 156 | } 157 | -------------------------------------------------------------------------------- /ak-update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$EUID" -ne 0 ]; then 3 | echo "请使用 root 权限运行此脚本" 4 | exit 1 5 | fi 6 | 7 | # 判断系统架构 8 | ARCH=$(uname -m) 9 | MONITOR_FILE="akile_monitor-linux-amd64" 10 | CLIENT_FILE="akile_client-linux-amd64" 11 | 12 | if [ "$ARCH" = "x86_64" ]; then 13 | MONITOR_FILE="akile_monitor-linux-amd64" 14 | CLIENT_FILE="akile_client-linux-amd64" 15 | elif [ "$ARCH" = "aarch64" ]; then 16 | MONITOR_FILE="akile_monitor-linux-arm64" 17 | CLIENT_FILE="akile_client-linux-arm64" 18 | elif [ "$ARCH" = "x86_64" ] && [ "$(uname -s)" = "Darwin" ]; then 19 | MONITOR_FILE="akile_monitor-darwin-amd64" 20 | CLIENT_FILE="akile_client-darwin-amd64" 21 | else 22 | echo "不支持的系统架构: $ARCH" 23 | exit 1 24 | fi 25 | 26 | function update_monitor_fe() { 27 | echo "正在更新主控前端..." 28 | 29 | # 检查是否安装了主控前端 30 | if [ ! -d "/etc/ak_monitor/frontend" ]; then 31 | echo "未检测到主控前端安装,请先安装主控前端" 32 | return 33 | fi 34 | 35 | cd /etc/ak_monitor/frontend 36 | 37 | # 创建临时目录 38 | echo "创建临时目录..." 39 | TEMP_DIR=$(mktemp -d) 40 | 41 | # 下载到临时目录 42 | echo "下载新版本..." 43 | cd "$TEMP_DIR" 44 | wget -O frontend.zip https://github.com/akile-network/akile_monitor_fe/releases/download/v.0.0.2/akile_monitor_fe.zip 45 | 46 | # 备份当前版本 47 | echo "备份当前版本..." 48 | timestamp=$(date +%Y%m%d_%H%M%S) 49 | mkdir -p /etc/ak_monitor/backup 50 | cd /etc/ak_monitor/frontend 51 | tar -czf "/etc/ak_monitor/backup/frontend_${timestamp}.tar.gz" ./* 52 | 53 | # 在临时目录解压并处理 54 | echo "解压新版本..." 55 | cd "$TEMP_DIR" 56 | unzip -o frontend.zip 57 | rm frontend.zip 58 | 59 | # 如果存在 config.json,删除它 60 | if [ -f "$TEMP_DIR/config.json" ]; then 61 | echo "移除新版本中的配置文件..." 62 | rm "$TEMP_DIR/config.json" 63 | fi 64 | 65 | # 复制新文件到目标目录,排除 config.json 66 | echo "更新文件..." 67 | cp -rf "$TEMP_DIR"/* /etc/ak_monitor/frontend/ 68 | 69 | # 清理临时目录 70 | echo "清理临时文件..." 71 | rm -rf "$TEMP_DIR" 72 | 73 | # 重启 Caddy 服务 74 | echo "重启 Caddy 服务..." 75 | systemctl restart caddy 76 | 77 | echo "主控前端更新完成!" 78 | } 79 | 80 | function update_monitor() { 81 | echo "正在更新主控后端..." 82 | 83 | # 检查是否安装了主控后端 84 | if [ ! -f "/etc/ak_monitor/config.json" ]; then 85 | echo "未检测到主控后端安装,请先安装主控后端" 86 | return 87 | fi 88 | 89 | # 停止服务 90 | systemctl stop ak_monitor 91 | 92 | cd /etc/ak_monitor/ 93 | 94 | # 备份当前版本 95 | echo "备份当前版本..." 96 | timestamp=$(date +%Y%m%d_%H%M%S) 97 | cp ak_monitor "ak_monitor.backup.${timestamp}" 98 | 99 | # 下载新版本 100 | echo "下载新版本..." 101 | wget -O ak_monitor https://github.com/akile-network/akile_monitor/releases/latest/download/$MONITOR_FILE 102 | chmod 777 ak_monitor 103 | 104 | # 重启服务 105 | systemctl start ak_monitor 106 | 107 | echo "主控后端更新完成!" 108 | } 109 | 110 | function update_client() { 111 | echo "正在更新被控..." 112 | 113 | # 检查是否安装了被控 114 | if [ ! -f "/etc/ak_monitor/client.json" ]; then 115 | echo "未检测到被控安装,请先安装被控" 116 | return 117 | fi 118 | 119 | # 停止服务 120 | systemctl stop ak_client 121 | 122 | cd /etc/ak_monitor/ 123 | 124 | # 备份当前版本 125 | echo "备份当前版本..." 126 | timestamp=$(date +%Y%m%d_%H%M%S) 127 | cp client "client.backup.${timestamp}" 128 | 129 | # 下载新版本 130 | echo "下载新版本..." 131 | wget -O client https://github.com/akile-network/akile_monitor/releases/latest/download/$CLIENT_FILE 132 | chmod 777 client 133 | 134 | # 重启服务 135 | systemctl start ak_client 136 | 137 | echo "被控更新完成!" 138 | } 139 | 140 | # 主菜单 141 | while true; do 142 | clear 143 | echo "==================================================" 144 | echo "AkileCloud Monitor 更新脚本" 145 | echo "==================================================" 146 | echo "1. 更新主控前端" 147 | echo "2. 更新主控后端" 148 | echo "3. 更新被控" 149 | echo "4. 退出" 150 | echo "==================================================" 151 | 152 | read -p "请选择要执行的操作 (1-4): " choice 153 | 154 | case $choice in 155 | 1) 156 | update_monitor_fe 157 | ;; 158 | 2) 159 | update_monitor 160 | ;; 161 | 3) 162 | update_client 163 | ;; 164 | 4) 165 | echo "退出脚本" 166 | exit 0 167 | ;; 168 | *) 169 | echo "无效的选项,请重新选择" 170 | ;; 171 | esac 172 | 173 | echo 174 | read -p "按回车键继续..." 175 | done 176 | -------------------------------------------------------------------------------- /DOCKER.md: -------------------------------------------------------------------------------- 1 | # Docker 部署介绍 2 | 3 | - 将前端[akile_monitor_fe](https://github.com/akile-network/akile_monitor_fe),主控服务[ak_monitor](https://github.com/akile-network/akile_monitor)以及被控客户端[ak_client](https://github.com/akile-network/akile_monitor)打包进容器,并利用 GitHub Actions 自动构建Docker镜像并推送至Docker Hub 4 | - 前端 端口80 主控服务端 端口3000 可自行映射到宿主机或反向代理TLS加密 5 | 6 | ## 支持架构 7 | 8 | - linux/amd64 9 | - linux/arm64 10 | 11 | # 准备工作 12 | 13 | > *以下所有 `/CHANGE_PATH` 替换为你的宿主机路径* 14 | > *SQLite 数据库需要提前创建,避免Docker自动创建文件夹导致失败* 15 | 16 | - [Docker](https://docs.docker.com/get-started/get-docker/) 安装 17 | 18 | ## 主控服务端 19 | 20 | - SQLite数据库 `/CHANGE_PATH/akile_monitor/ak_monitor.db` 21 | 22 | # [compose文件](./docker-compose.yml) 23 | 24 | # 主控服务端 25 | 26 | - 环境变量(默认) 与 配置文件 二选一即可 27 | 28 | - 如需映射配置文件,请提前创建文件并挂载至目录 `/CHANGE_PATH/akile_monitor/server/config.json:/app/config.json` 29 | 30 | - [主控服务端配置文件参考](https://github.com/akile-network/akile_monitor/blob/main/config.json) 31 | 32 | ## Docker Cli 部署 33 | 34 | ``` 35 | docker run -it --name akile_monitor_server --restart always -v /CHANGE_PATH/akile_monitor/server/ak_monitor.db:/app/ak_monitor.db -e AUTH_SECRET="auth_secret" -e LISTEN=":3000" -e ENABLE_TG=false -e TG_TOKEN="your_telegram_bot_token" -e HOOK_URI="/hook" -e UPDATE_URI="/monitor" -e WEB_URI="/ws" -e HOOK_TOKEN="hook_token" -e TG_CHAT_ID=0 -p 3000:3000 -e TZ="Asia/Shanghai" niliaerith/akile_monitor_server 36 | ``` 37 | 38 | ## Docker Compose 部署 39 | 40 | ```compose.yml 41 | cat < server-compose.yml 42 | services: 43 | akile_monitor_server: 44 | image: niliaerith/akile_monitor_server 45 | container_name: akile_monitor_server 46 | hostname: akile_monitor_server 47 | restart: always 48 | ports: 49 | - 3000:3000 #主控服务端 端口 50 | volumes: 51 | - /CHANGE_PATH/akile_monitor/server/ak_monitor.db:/app/ak_monitor.db 52 | environment: 53 | TZ: "Asia/Shanghai" 54 | AUTH_SECRET: "auth_secret" 55 | LISTEN: ":3000" 56 | ENABLE_TG: false 57 | TG_TOKEN: "your_telegram_bot_token" 58 | HOOK_URI: "/hook" 59 | UPDATE_URI: "/monitor" 60 | WEB_URI: "/ws" 61 | HOOK_TOKEN: "hook_token" 62 | TG_CHAT_ID: 0 63 | EOF 64 | docker compose -f server-compose.yml up -d 65 | ``` 66 | 67 | # 前端 部署 68 | 69 | - 环境变量(默认) 与 配置文件 二选一即可 70 | 71 | - 如需映射配置文件,请提前创建文件并挂载至目录 `/CHANGE_PATH/akile_monitor/caddy/config.json:/usr/share/caddy/config.json` 72 | 73 | - 前端配置文件参考如下 74 | 75 | ``` 76 | { 77 | "socket": "ws://192.168.31.64:3000/ws", 78 | "apiURL": "http://192.168.31.64:3000" 79 | } 80 | ``` 81 | 82 | ## Docker Cli 部署 83 | 84 | ``` 85 | docker run -it --name akile_monitor_server --restart always -e SOCKET="ws://192.168.31.64:3000/ws" -e APIURL="http://192.168.31.64:3000" -p 80:80 -e TZ="Asia/Shanghai" niliaerith/akile_monitor_fe 86 | ``` 87 | 88 | ## Docker Compose 部署 89 | 90 | ```compose.yml 91 | cat < fe-compose.yml 92 | services: 93 | akile_monitor_fe: 94 | image: niliaerith/akile_monitor_fe 95 | container_name: akile_monitor_fe 96 | hostname: akile_monitor_fe 97 | restart: always 98 | ports: 99 | - 80:80 #前端 端口 100 | environment: 101 | TZ: "Asia/Shanghai" 102 | SOCKET: "ws://192.168.31.64:3000/ws" 103 | APIURL: "http://192.168.31.64:3000" 104 | EOF 105 | docker compose -f fe-compose.yml up -d 106 | ``` 107 | 108 | # 被控客户端 部署 109 | 110 | - 必须添加 `host` 网络模式,否则识别的流量为容器内的 111 | - 必须添加 `/var/run/docker.sock` 卷,否则识别的系统为容器内的 112 | 113 | - 环境变量(默认) 与 配置文件 二选一即可 114 | 115 | - 如需映射配置文件,请提前创建文件并挂载至目录 `/CHANGE_PATH/akile_monitor/client/client.json:/app/client.json` 116 | 117 | - [被控客户端配置文件参考](https://github.com/akile-network/akile_monitor/blob/main/client.json) 118 | 119 | ## Docker Cli 部署 120 | 121 | ``` 122 | docker run -it --name akile_monitor_client --restart always -e AUTH_SECRET="auth_secret" -e URL="ws://localhost:3000/monitor" -e NET_NAME="eth0" -e NAME="HK-Akile" -v /var/run/docker.sock:/var/run/docker.sock --net host -e TZ="Asia/Shanghai" niliaerith/akile_monitor_client 123 | ``` 124 | 125 | ## Docker Compose 部署 126 | 127 | ```compose.yml 128 | cat < client-compose.yml 129 | services: 130 | akile_monitor_client: 131 | image: niliaerith/akile_monitor_client 132 | container_name: akile_monitor_client 133 | hostname: akile_monitor_client 134 | restart: always 135 | network_mode: host 136 | volumes: 137 | - /var/run/docker.sock:/var/run/docker.sock 138 | environment: 139 | TZ: "Asia/Shanghai" 140 | AUTH_SECRET: "auth_secret" 141 | URL: "ws://localhost:3000/monitor" 142 | NET_NAME: "eth0" 143 | NAME: "HK-Akile" 144 | EOF 145 | docker compose -f client-compose.yml up -d 146 | ``` 147 | 148 | # Github Actions 自动构建镜像 149 | 150 | - Fork项目 151 | - Settings > Secrets and variables > Actions > New repository secret 添加 `DOCKER_USERNAME` (你的 Docker Hub 用户名) 和 `DOCKER_PASSWORD` (你的 Docker Hub 密码) 两个变量 152 | - Actions 中手动开始工作流 或者 主页Star 或者 修改任意README文档后push触发 153 | 154 | # Docker Build 本地构建镜像 155 | 156 | ``` 157 | git clone https://github.com/akile-network/akile_monitor 158 | cd akile_monitor 159 | docker build --target server --tag akile_monitor_server . 160 | docker build --target fe --tag akile_monitor_fe . 161 | docker build --target client --tag akile_monitor_client . 162 | ``` 163 | 164 | # 已知问题 165 | 166 | > *因为被控客户端在Docker alpine容器内,所以虚拟化始终显示为`docker`*。 167 | - 解决方法1: 被控客户端采用 二进制部署,详见 [被控端](./README.md) 168 | - 解决方法2: 忽略虚拟化显示内容 169 | -------------------------------------------------------------------------------- /setup-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if script is run as root 3 | if [ "$EUID" -ne 0 ]; then 4 | echo "Please run as root" 5 | exit 1 6 | fi 7 | 8 | # Install bc based on system package manager 9 | if command -v apt-get > /dev/null; then 10 | apt-get update && apt-get install -y bc 11 | elif command -v yum > /dev/null; then 12 | yum update -y && yum install -y bc 13 | else 14 | echo "Could not install bc. Please install it manually." 15 | exit 1 16 | fi 17 | 18 | # Stop existing service if running 19 | systemctl stop ak_client 20 | 21 | # Function to detect main network interface 22 | get_main_interface() { 23 | local interfaces=$(ip -o link show | \ 24 | awk -F': ' '$2 !~ /^((lo|docker|veth|br-|virbr|tun|vnet|wg|vmbr|dummy|gre|sit|vlan|lxc|lxd|warp|tap))/{print $2}' | \ 25 | grep -v '@') 26 | 27 | local interface_count=$(echo "$interfaces" | wc -l) 28 | 29 | # 格式化流量大小的函数 30 | format_bytes() { 31 | local bytes=$1 32 | if [ $bytes -lt 1024 ]; then 33 | echo "${bytes} B" 34 | elif [ $bytes -lt 1048576 ]; then # 1024*1024 35 | echo "$(echo "scale=2; $bytes/1024" | bc) KB" 36 | elif [ $bytes -lt 1048576 ]; then # 1024*1024 37 | echo "$(echo "scale=2; $bytes/1024" | bc) KB" 38 | elif [ $bytes -lt 1073741824 ]; then # 1024*1024*1024 39 | echo "$(echo "scale=2; $bytes/1024/1024" | bc) MB" 40 | elif [ $bytes -lt 1099511627776 ]; then # 1024*1024*1024*1024 41 | echo "$(echo "scale=2; $bytes/1024/1024/1024" | bc) GB" 42 | else 43 | echo "$(echo "scale=2; $bytes/1024/1024/1024/1024" | bc) TB" 44 | fi 45 | } 46 | 47 | # 显示网卡流量的函数 48 | show_interface_traffic() { 49 | local interface=$1 50 | if [ -d "/sys/class/net/$interface" ]; then 51 | local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes) 52 | local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes) 53 | echo " ↓ Received: $(format_bytes $rx_bytes)" 54 | echo " ↑ Sent: $(format_bytes $tx_bytes)" 55 | else 56 | echo " 无法读取流量信息" 57 | fi 58 | } 59 | 60 | # 如果没有找到合适的接口或有多个接口时显示所有可用接口 61 | echo "所有可用的网卡接口:" >&2 62 | echo "------------------------" >&2 63 | local i=1 64 | while read -r interface; do 65 | echo "$i) $interface" >&2 66 | show_interface_traffic "$interface" >&2 67 | i=$((i+1)) 68 | done < <(ip -o link show | grep -v "lo:" | awk -F': ' '{print $2}') 69 | echo "------------------------" >&2 70 | 71 | while true; do 72 | read -p "请选择网卡,如上方显示异常或没有需要的网卡,请直接填入网卡名: " selection 73 | 74 | # 检查是否为数字 75 | if [[ "$selection" =~ ^[0-9]+$ ]]; then 76 | # 如果是数字,检查是否在有效范围内 77 | selected_interface=$(ip -o link show | grep -v "lo:" | sed -n "${selection}p" | awk -F': ' '{print $2}') 78 | if [ -n "$selected_interface" ]; then 79 | echo "已选择网卡: $selected_interface" >&2 80 | echo "$selected_interface" 81 | break 82 | else 83 | echo "无效的选择,请重新输入" >&2 84 | continue 85 | fi 86 | else 87 | # 直接使用输入的网卡名 88 | echo "已选择网卡: $selection" >&2 89 | echo "$selection" 90 | break 91 | fi 92 | done 93 | } 94 | 95 | # Check if all arguments are provided 96 | if [ "$#" -ne 3 ]; then 97 | echo "Usage: $0 " 98 | echo "Example: $0 your_secret wss://api.123.321 HK-Akile" 99 | exit 1 100 | fi 101 | 102 | # Get system architecture 103 | ARCH=$(uname -m) 104 | CLIENT_FILE="akile_client-linux-amd64" 105 | 106 | # Set appropriate client file based on architecture 107 | if [ "$ARCH" = "x86_64" ]; then 108 | CLIENT_FILE="akile_client-linux-amd64" 109 | elif [ "$ARCH" = "aarch64" ]; then 110 | CLIENT_FILE="akile_client-linux-arm64" 111 | elif [ "$ARCH" = "x86_64" ] && [ "$(uname -s)" = "Darwin" ]; then 112 | CLIENT_FILE="akile_client-darwin-amd64" 113 | else 114 | echo "Unsupported architecture: $ARCH" 115 | exit 1 116 | fi 117 | 118 | # Assign command line arguments to variables 119 | auth_secret="$1" 120 | url="$2" 121 | monitor_name="$3" 122 | 123 | # Get network interface 124 | net_name=$(get_main_interface) 125 | echo "Using network interface: $net_name" 126 | 127 | # Create directory and change to it 128 | mkdir -p /etc/ak_monitor/ 129 | cd /etc/ak_monitor/ 130 | 131 | # Download client 132 | wget -O client https://github.com/akile-network/akile_monitor/releases/latest/download/$CLIENT_FILE 133 | chmod 777 client 134 | 135 | # Create systemd service file 136 | cat > /etc/systemd/system/ak_client.service << 'EOF' 137 | [Unit] 138 | Description=AkileCloud Monitor Service 139 | After=network.target nss-lookup.target 140 | Wants=network.target 141 | 142 | [Service] 143 | User=root 144 | Group=root 145 | Type=simple 146 | LimitAS=infinity 147 | LimitRSS=infinity 148 | LimitCORE=infinity 149 | LimitNOFILE=999999999 150 | WorkingDirectory=/etc/ak_monitor/ 151 | ExecStart=/etc/ak_monitor/client 152 | Restart=always 153 | RestartSec=1 154 | 155 | [Install] 156 | WantedBy=multi-user.target 157 | EOF 158 | 159 | # Create client configuration 160 | cat > /etc/ak_monitor/client.json << EOF 161 | { 162 | "auth_secret": "${auth_secret}", 163 | "url": "${url}", 164 | "net_name": "${net_name}", 165 | "name": "${monitor_name}" 166 | } 167 | EOF 168 | 169 | # Set proper permissions 170 | chmod 644 /etc/ak_monitor/client.json 171 | chmod 644 /etc/systemd/system/ak_client.service 172 | 173 | # Reload systemd and enable service 174 | systemctl daemon-reload 175 | systemctl enable ak_client.service 176 | systemctl start ak_client.service 177 | 178 | echo "Installation complete! Service status:" 179 | systemctl status ak_client.service 180 | -------------------------------------------------------------------------------- /alpine-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Check if script is run as root 3 | if [ "$EUID" -ne 0 ]; then 4 | echo "Please run as root" 5 | exit 1 6 | fi 7 | 8 | # Install bc based on system package manager 9 | if command -v apk > /dev/null; then 10 | apk update && apk add bc 11 | else 12 | echo "It's not alpine." 13 | exit 1 14 | fi 15 | 16 | # Stop existing service if running 17 | rc-service ak_client stop 18 | 19 | # Function to detect main network interface 20 | get_main_interface() { 21 | local interfaces=$(ip -o link show | \ 22 | awk -F': ' '$2 !~ /^((lo|docker|veth|br-|virbr|tun|vnet|wg|vmbr|dummy|gre|sit|vlan|lxc|lxd|warp|tap))/{print $2}' | \ 23 | grep -v '@') 24 | 25 | local interface_count=$(echo "$interfaces" | wc -l) 26 | 27 | # 格式化流量大小的函数 28 | format_bytes() { 29 | local bytes=$1 30 | if [ $bytes -lt 1024 ]; then 31 | echo "${bytes} B" 32 | elif [ $bytes -lt 1048576 ]; then # 1024*1024 33 | echo "$(echo "scale=2; $bytes/1024" | bc) KB" 34 | elif [ $bytes -lt 1073741824 ]; then # 1024*1024*1024 35 | echo "$(echo "scale=2; $bytes/1024/1024" | bc) MB" 36 | elif [ $bytes -lt 1099511627776 ]; then # 1024*1024*1024*1024 37 | echo "$(echo "scale=2; $bytes/1024/1024/1024" | bc) GB" 38 | else 39 | echo "$(echo "scale=2; $bytes/1024/1024/1024/1024" | bc) TB" 40 | fi 41 | } 42 | 43 | # 显示网卡流量的函数 44 | show_interface_traffic() { 45 | local interface=$1 46 | if [ -d "/sys/class/net/$interface" ]; then 47 | local rx_bytes=$(cat /sys/class/net/$interface/statistics/rx_bytes) 48 | local tx_bytes=$(cat /sys/class/net/$interface/statistics/tx_bytes) 49 | echo " ↓ Received: $(format_bytes $rx_bytes)" 50 | echo " ↑ Sent: $(format_bytes $tx_bytes)" 51 | else 52 | echo " 无法读取流量信息" 53 | fi 54 | } 55 | 56 | # 如果没有找到合适的接口或有多个接口时显示所有可用接口 57 | echo "所有可用的网卡接口:" >&2 58 | echo "------------------------" >&2 59 | local i=1 60 | while read -r interface; do 61 | echo "$i) $interface" >&2 62 | show_interface_traffic "$interface" >&2 63 | i=$((i+1)) 64 | done < <(ip -o link show | grep -v "lo:" | awk -F': ' '{print $2}') 65 | echo "------------------------" >&2 66 | 67 | while true; do 68 | read -p "请选择网卡,如上方显示异常或没有需要的网卡,请直接填入网卡名: " selection 69 | 70 | # 检查是否为数字 71 | if [[ "$selection" =~ ^[0-9]+$ ]]; then 72 | # 如果是数字,检查是否在有效范围内 73 | selected_interface=$(ip -o link show | grep -v "lo:" | sed -n "${selection}p" | awk -F': ' '{print $2}') 74 | if [ -n "$selected_interface" ]; then 75 | echo "已选择网卡: $selected_interface" >&2 76 | echo "$selected_interface" 77 | break 78 | else 79 | echo "无效的选择,请重新输入" >&2 80 | continue 81 | fi 82 | else 83 | # 直接使用输入的网卡名 84 | echo "已选择网卡: $selection" >&2 85 | echo "$selection" 86 | break 87 | fi 88 | done 89 | } 90 | 91 | # Check if all arguments are provided 92 | if [ "$#" -ne 3 ]; then 93 | echo "Usage: $0 " 94 | echo "Example: $0 your_secret wss://api.123.321 HK-Akile" 95 | exit 1 96 | fi 97 | 98 | # Get system architecture 99 | ARCH=$(uname -m) 100 | CLIENT_FILE="akile_client-linux-amd64" 101 | 102 | # Set appropriate client file based on architecture 103 | if [ "$ARCH" = "x86_64" ]; then 104 | CLIENT_FILE="akile_client-linux-amd64" 105 | elif [ "$ARCH" = "aarch64" ]; then 106 | CLIENT_FILE="akile_client-linux-arm64" 107 | elif [ "$ARCH" = "x86_64" ] && [ "$(uname -s)" = "Darwin" ]; then 108 | CLIENT_FILE="akile_client-darwin-amd64" 109 | else 110 | echo "Unsupported architecture: $ARCH" 111 | exit 1 112 | fi 113 | 114 | # Assign command line arguments to variables 115 | auth_secret="$1" 116 | url="$2" 117 | monitor_name="$3" 118 | 119 | # Get network interface 120 | net_name=$(get_main_interface) 121 | echo "Using network interface: $net_name" 122 | 123 | # Create directory and change to it 124 | mkdir -p /etc/ak_monitor/ 125 | cd /etc/ak_monitor/ 126 | 127 | # Download client 128 | wget -O client https://github.com/akile-network/akile_monitor/releases/latest/download/$CLIENT_FILE 129 | chmod 777 client 130 | 131 | # Create openrc service file 132 | cat > /etc/init.d/ak_client << 'EOF' 133 | #!/sbin/openrc-run 134 | 135 | # 定义服务的名称变量 136 | name="AkileCloud Monitor Service" 137 | 138 | # 定义服务的执行路径,将使用该路径启动服务程序 139 | command="/etc/ak_monitor/client" 140 | 141 | # 定义服务的启动参数 142 | # command_args="--param1 value1 --param2 value2" 143 | 144 | # 提供服务的描述信息 145 | description="Custom service for ${name}" 146 | 147 | # 定义服务的依赖关系 148 | depend() { 149 | # 指定该服务需要网络(net 服务)支持 150 | need net 151 | } 152 | 153 | # 在启动服务之前的预处理函数 154 | start_pre() { 155 | # 打印启动前的消息 156 | ebegin "Preparing to start ${name}" 157 | # 可以在这里添加任何启动服务之前的准备工作 158 | eend $? # eend 会输出函数操作的结果状态,$? 表示上一个命令的返回值 159 | } 160 | 161 | # 启动服务的函数 162 | start() { 163 | # 打印启动消息 164 | ebegin "Starting ${name}" 165 | # 切换到工作目录 166 | cd /etc/ak_monitor/ 167 | # 使用 start-stop-daemon 命令启动服务 168 | start-stop-daemon --start --exec ${command} 169 | eend $? # 输出启动操作的结果状态 170 | } 171 | 172 | # 停止服务的函数 173 | stop() { 174 | # 打印停止消息 175 | ebegin "Stopping ${name}" 176 | # 使用 start-stop-daemon 命令停止服务 177 | start-stop-daemon --stop --exec ${command} 178 | eend $? # 输出停止操作的结果状态 179 | } 180 | 181 | # 重新启动服务的函数 182 | restart() { 183 | # 打印重新启动消息 184 | ebegin "Restarting ${name}" 185 | # 停止服务 186 | start-stop-daemon --stop --exec ${command} 187 | # 等待 1 秒钟,确保服务完全停止 188 | sleep 1 189 | # 启动服务 190 | start-stop-daemon --start --exec ${command} 191 | eend $? # 输出重新启动操作的结果状态 192 | } 193 | EOF 194 | 195 | # Create client configuration 196 | cat > /etc/ak_monitor/client.json << EOF 197 | { 198 | "auth_secret": "${auth_secret}", 199 | "url": "${url}", 200 | "net_name": "${net_name}", 201 | "name": "${monitor_name}" 202 | } 203 | EOF 204 | 205 | # Set proper permissions 206 | chmod 644 /etc/ak_monitor/client.json 207 | chmod 755 /etc/init.d/ak_client 208 | 209 | # Add service to default runlevel 210 | rc-update add ak_client default 211 | 212 | # Start service 213 | rc-service ak_client start 214 | 215 | echo "Installation complete! Service status:" 216 | rc-service ak_client status 217 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "akile_monitor/client/model" 5 | "bytes" 6 | "compress/gzip" 7 | "context" 8 | "fmt" 9 | "github.com/cloudwego/hertz/pkg/common/json" 10 | "io" 11 | "log" 12 | "regexp" 13 | "sort" 14 | "strconv" 15 | "time" 16 | 17 | "github.com/cloudwego/hertz/pkg/app" 18 | "github.com/cloudwego/hertz/pkg/app/server" 19 | "github.com/glebarez/sqlite" 20 | "github.com/hertz-contrib/cors" 21 | "github.com/hertz-contrib/websocket" 22 | "gorm.io/gorm" 23 | ) 24 | 25 | type Data struct { 26 | Name string `gorm:"primaryKey"` 27 | Data string 28 | } 29 | type M struct { 30 | Host *model.Host 31 | State *model.HostState 32 | TimeStamp int64 33 | } 34 | 35 | var db *gorm.DB 36 | var filedb *gorm.DB 37 | 38 | func initDb() { 39 | var dbfile = "file::memory:?cache=shared" 40 | Db, err := gorm.Open(sqlite.Open(dbfile), &gorm.Config{}) 41 | if err != nil { 42 | log.Panic(err) 43 | } 44 | 45 | Db.AutoMigrate(&Data{}) 46 | db = Db 47 | } 48 | 49 | func initFileDb() { 50 | var dbfile = "ak_monitor.db" 51 | Db, err := gorm.Open(sqlite.Open(dbfile), &gorm.Config{}) 52 | if err != nil { 53 | log.Panic(err) 54 | } 55 | 56 | Db.AutoMigrate(&Host{}) 57 | filedb = Db 58 | } 59 | 60 | type Host struct { 61 | Name string `json:"name" gorm:"primaryKey"` 62 | DueTime int64 `json:"due_time"` // 到期时间 63 | BuyUrl string `json:"buy_url"` // 购买链接 64 | Seller string `json:"seller"` // 卖家 65 | Price string `json:"price"` // 价格 66 | } 67 | 68 | var upgrader = websocket.HertzUpgrader{ 69 | CheckOrigin: func(r *app.RequestContext) bool { 70 | return true // 允许所有跨域请求 71 | }, 72 | EnableCompression: true, 73 | } // use default options 74 | 75 | func monitor(_ context.Context, c *app.RequestContext) { 76 | err := upgrader.Upgrade(c, func(conn *websocket.Conn) { 77 | var authed bool 78 | for { 79 | mt, message, err := conn.ReadMessage() 80 | if err != nil { 81 | log.Printf("client: %s,read: %s\n", c.ClientIP(), err.Error()) 82 | break 83 | } 84 | 85 | if !authed { 86 | if string(message) != cfg.AuthSecret { 87 | log.Printf("client: %s,auth failed\n", c.ClientIP()) 88 | break 89 | } 90 | authed = true 91 | err = conn.WriteMessage(mt, []byte("auth success")) 92 | if err != nil { 93 | log.Printf("client: %s,write: %s\n", c.ClientIP(), err.Error()) 94 | break 95 | } 96 | continue 97 | } 98 | 99 | //gzip解压 100 | var buf bytes.Buffer 101 | buf.Write(message) 102 | r, _ := gzip.NewReader(&buf) 103 | message, _ = io.ReadAll(r) 104 | r.Close() 105 | 106 | var d M 107 | 108 | err = json.Unmarshal(message, &d) 109 | if err != nil { 110 | log.Printf("client: %s,unmarshal: %s\n", c.ClientIP(), err.Error()) 111 | break 112 | } 113 | 114 | var data Data 115 | db.Model(&Data{}).Where("name = ?", d.Host.Name).First(&data) 116 | if data.Name == "" { 117 | db.Create(&Data{Name: d.Host.Name, Data: string(message)}) 118 | } else { 119 | db.Model(&Data{}).Where("name = ?", d.Host.Name).Update("data", string(message)) 120 | } 121 | } 122 | }) 123 | if err != nil { 124 | log.Printf("client: %s,upgrade: %s\n", c.ClientIP(), err.Error()) 125 | return 126 | } 127 | } 128 | 129 | var offline = make(map[string]bool) 130 | 131 | func main() { 132 | LoadConfig() 133 | initDb() 134 | initFileDb() 135 | if cfg.EnableTG { 136 | go startbot() 137 | } 138 | 139 | if cfg.TgChatID != 0 { 140 | go func() { 141 | for { 142 | var mm []M 143 | data := fetchData() 144 | json.Unmarshal(data, &mm) 145 | for _, v := range mm { 146 | // 30秒内离线 147 | if v.TimeStamp < time.Now().Unix()-60 { 148 | if !offline[v.Host.Name] { 149 | offline[v.Host.Name] = true 150 | msg := fmt.Sprintf("❌ %s 离线了", v.Host.Name) 151 | SendTGMessage(msg) 152 | } 153 | } else { 154 | if offline[v.Host.Name] { 155 | offline[v.Host.Name] = false 156 | msg := fmt.Sprintf("✅ %s 上线了", v.Host.Name) 157 | SendTGMessage(msg) 158 | } 159 | } 160 | } 161 | time.Sleep(time.Second * 20) 162 | 163 | } 164 | }() 165 | } 166 | h := server.Default(server.WithHostPorts(cfg.Listen)) 167 | config := cors.DefaultConfig() 168 | config.AllowAllOrigins = true 169 | h.Use(cors.New(config)) 170 | h.NoHijackConnPool = true 171 | h.GET("/info", Info) 172 | h.POST("/info", UpdateInfo) 173 | h.GET(cfg.UpdateUri, monitor) 174 | h.GET(cfg.WebUri, ws) 175 | h.GET(cfg.HookUri, Hook) 176 | h.POST("/delete", DeleteHost) 177 | h.Spin() 178 | } 179 | 180 | func Hook(_ context.Context, c *app.RequestContext) { 181 | token := c.Query("token") 182 | if token != cfg.HookToken { 183 | c.JSON(401, "auth failed") 184 | return 185 | } 186 | data := fetchData() 187 | c.JSON(200, data) 188 | } 189 | 190 | func Info(_ context.Context, c *app.RequestContext) { 191 | var ret []*Host 192 | err := filedb.Model(&Host{}).Find(&ret).Error 193 | if err != nil { 194 | log.Println(err) 195 | c.JSON(200, "[]") 196 | return 197 | } 198 | c.JSON(200, ret) 199 | } 200 | 201 | type UpdateRequest struct { 202 | AuthSecret string `json:"auth_secret"` 203 | Host 204 | } 205 | 206 | func UpdateInfo(_ context.Context, c *app.RequestContext) { 207 | var ret UpdateRequest 208 | err := c.BindJSON(&ret) 209 | if err != nil { 210 | c.JSON(400, "bad request") 211 | return 212 | } 213 | 214 | if ret.AuthSecret != cfg.AuthSecret { 215 | c.JSON(401, "auth failed") 216 | return 217 | } 218 | 219 | var h Host 220 | 221 | filedb.Model(&Host{}).Where("name = ?", ret.Name).First(&h) 222 | if h.Name == "" { 223 | h = ret.Host 224 | filedb.Model(&Host{}).Create(&h) 225 | } else { 226 | h = ret.Host 227 | filedb.Model(&Host{}).Where("name = ?", ret.Name).Save(&h) 228 | } 229 | c.JSON(200, "ok") 230 | } 231 | 232 | type DeleteHostRequest struct { 233 | AuthSecret string `json:"auth_secret"` 234 | Name string `json:"name"` 235 | } 236 | 237 | func DeleteHost(_ context.Context, c *app.RequestContext) { 238 | var req DeleteHostRequest 239 | err := c.BindJSON(&req) 240 | if err != nil { 241 | c.JSON(400, "bad request") 242 | return 243 | } 244 | 245 | if req.AuthSecret != cfg.AuthSecret { 246 | c.JSON(401, "auth failed") 247 | return 248 | } 249 | 250 | var data Data 251 | db.Model(&Data{}).Where("name = ?", req.Name).First(&data) 252 | if data.Name == "" { 253 | c.JSON(404, "not found") 254 | return 255 | } 256 | 257 | db.Delete(&Data{}, "name = ?", req.Name) 258 | c.JSON(200, "ok") 259 | } 260 | 261 | func ws(_ context.Context, c *app.RequestContext) { 262 | err := upgrader.Upgrade(c, func(conn *websocket.Conn) { 263 | for { 264 | _, _, err := conn.ReadMessage() 265 | if err != nil { 266 | log.Printf("client: %s,read: %s\n", c.ClientIP(), err.Error()) 267 | break 268 | } 269 | 270 | data := fetchData() 271 | err = conn.WriteMessage(websocket.TextMessage, append([]byte("data: "), data...)) 272 | if err != nil { 273 | log.Printf("client: %s,write: %s\n", c.ClientIP(), err.Error()) 274 | break 275 | } 276 | } 277 | }) 278 | if err != nil { 279 | log.Printf("client: %s,upgrade: %s\n", c.ClientIP(), err.Error()) 280 | return 281 | } 282 | } 283 | 284 | func fetchData() []byte { 285 | // 模拟数据获取 286 | var ret []Data 287 | db.Model(&Data{}).Find(&ret) 288 | 289 | var mm []M 290 | 291 | //排序根据Name 10在9后面 292 | sort.Slice(ret, func(i, j int) bool { 293 | return compareStrings(ret[i].Name, ret[j].Name) < 0 294 | }) 295 | 296 | //var jsonData string 297 | for _, v := range ret { 298 | var m M 299 | json.Unmarshal([]byte(v.Data), &m) 300 | mm = append(mm, m) 301 | } 302 | 303 | jsonData, _ := json.Marshal(mm) 304 | return jsonData 305 | } 306 | 307 | // 定义一个函数来比较两个带字母和数字的字符串 308 | func compareStrings(str1, str2 string) int { 309 | //先去除空格 310 | str1 = regexp.MustCompile(`\s+`).ReplaceAllString(str1, "") 311 | str2 = regexp.MustCompile(`\s+`).ReplaceAllString(str2, "") 312 | 313 | // 使用正则表达式提取字母和数字部分 314 | re := regexp.MustCompile(`([a-zA-Z]+)(\d*)`) 315 | matches1 := re.FindStringSubmatch(str1) 316 | matches2 := re.FindStringSubmatch(str2) 317 | 318 | if len(matches1) != 3 || len(matches2) != 3 { 319 | return 0 // 格式不匹配 320 | } 321 | 322 | // 提取字母部分 323 | letter1 := matches1[1] 324 | letter2 := matches2[1] 325 | 326 | // 提取并转换数字部分 327 | num1 := 0 328 | num2 := 0 329 | if len(matches1[2]) > 0 { 330 | num1, _ = strconv.Atoi(matches1[2]) 331 | } 332 | if len(matches2[2]) > 0 { 333 | num2, _ = strconv.Atoi(matches2[2]) 334 | } 335 | 336 | // 先比较字母部分,逐个字符比较 337 | for i := 0; i < len(letter1) && i < len(letter2); i++ { 338 | if letter1[i] < letter2[i] { 339 | return -1 340 | } else if letter1[i] > letter2[i] { 341 | return 1 342 | } 343 | } 344 | 345 | // 如果字母部分相同,长度不等时,短的字母部分小 346 | if len(letter1) < len(letter2) { 347 | return -1 348 | } else if len(letter1) > len(letter2) { 349 | return 1 350 | } 351 | 352 | // 字母相同,比较数字部分 353 | if num1 < num2 { 354 | return -1 355 | } else if num1 > num2 { 356 | return 1 357 | } 358 | 359 | // 如果字母和数字都相同 360 | return 0 361 | } 362 | -------------------------------------------------------------------------------- /tgbot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "strconv" 9 | "time" 10 | 11 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 12 | ) 13 | 14 | func startbot() { 15 | bot, err := tgbotapi.NewBotAPI(cfg.TgToken) 16 | if err != nil { 17 | log.Println("Error creating bot", err) 18 | return 19 | } 20 | 21 | bot.Debug = false 22 | 23 | log.Printf("Authorized on account %s", bot.Self.UserName) 24 | 25 | setBotCommands(bot) 26 | 27 | u := tgbotapi.NewUpdate(0) 28 | u.Timeout = 60 29 | 30 | updates := bot.GetUpdatesChan(u) 31 | 32 | for update := range updates { 33 | if update.Message != nil { // If we got a message 34 | log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) 35 | 36 | if update.Message.IsCommand() { 37 | if update.Message.Command() == "akall" { 38 | 39 | var ret []Data 40 | db.Model(&Data{}).Find(&ret) 41 | 42 | var mm []M 43 | for _, v := range ret { 44 | var m M 45 | json.Unmarshal([]byte(v.Data), &m) 46 | mm = append(mm, m) 47 | } 48 | 49 | var online int 50 | var cpu int 51 | var mem uint64 52 | var memused uint64 53 | var downspeed uint64 54 | var upspeed uint64 55 | var downflow uint64 56 | var upflow uint64 57 | var swapused uint64 58 | var swap uint64 59 | time_now := time.Now().Unix() 60 | for _, v := range mm { 61 | if v.TimeStamp > time_now-30 { 62 | online++ 63 | } 64 | cpu += parseCPU(v.Host.CPU[0]) 65 | mem += v.Host.MemTotal 66 | memused += v.State.MemUsed 67 | swap += v.Host.SwapTotal 68 | swapused += v.State.SwapUsed 69 | downspeed += v.State.NetInSpeed 70 | upspeed += v.State.NetOutSpeed 71 | downflow += v.State.NetInTransfer 72 | upflow += v.State.NetOutTransfer 73 | } 74 | 75 | //流量对等性 76 | var duideng string 77 | if downflow > upflow { 78 | duideng = fmt.Sprintf("%.2f%%", float64(upflow)/float64(downflow)*100) 79 | } else { 80 | duideng = fmt.Sprintf("%.2f%%", float64(downflow)/float64(upflow)*100) 81 | } 82 | 83 | msg := fmt.Sprintf(`统计信息 84 | =========================== 85 | 服务器数量: %d 86 | 在线服务器: %d 87 | CPU核心数: %d 88 | 内存: %s [%s/%s] 89 | 交换分区: %s [%s/%s] 90 | 下行速度: ↓%s/s 91 | 上行速度: ↑%s/s 92 | 下行流量: ↓%s 93 | 上行流量: ↑%s 94 | 流量对等性: %s 95 | 96 | 更新于:%s UTC`, 97 | len(mm), online, cpu, 98 | fmt.Sprintf("%.2f%%", float64(memused)/float64(mem)*100), 99 | formatSize(memused), formatSize(mem), 100 | fmt.Sprintf("%.2f%%", float64(swapused)/float64(swap)*100), 101 | formatSize(swapused), formatSize(swap), 102 | formatSize(downspeed), formatSize(upspeed), 103 | formatSize(downflow), formatSize(upflow), 104 | duideng, 105 | time.Now().UTC().Format("2006-01-02 15:04:05"), 106 | ) 107 | bot.Send(tgbotapi.NewMessage(update.Message.Chat.ID, msg)) 108 | } else if update.Message.Command() == "id" { 109 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, fmt.Sprintf("你的ID是: %d", update.Message.From.ID)) 110 | bot.Send(msg) 111 | } else if update.Message.Command() == "server" { 112 | var servers []Data 113 | db.Model(&Data{}).Find(&servers) 114 | 115 | if len(servers) == 0 { 116 | bot.Send(tgbotapi.NewMessage(update.Message.Chat.ID, "没有找到任何服务器信息。")) 117 | return 118 | } 119 | 120 | var rows [][]tgbotapi.InlineKeyboardButton 121 | row := []tgbotapi.InlineKeyboardButton{} 122 | for i, server := range servers { 123 | callbackData := fmt.Sprintf("/status %s", server.Name) 124 | row = append(row, tgbotapi.NewInlineKeyboardButtonData(server.Name, callbackData)) 125 | if (i+1)%2 == 0 { 126 | rows = append(rows, row) 127 | row = []tgbotapi.InlineKeyboardButton{} 128 | } 129 | } 130 | if len(row) > 0 { 131 | rows = append(rows, row) 132 | } 133 | 134 | keyboard := tgbotapi.NewInlineKeyboardMarkup(rows...) 135 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "请选择一个服务器:") 136 | msg.ReplyMarkup = keyboard 137 | bot.Send(msg) 138 | } else if update.Message.Command() == "status" { 139 | serverName := update.Message.CommandArguments() 140 | if serverName == "" { 141 | bot.Send(tgbotapi.NewMessage(update.Message.Chat.ID, "用法: /status <服务器名称>")) 142 | } else { 143 | sendOrEditServerStatus(bot, update.Message.Chat.ID, 0, serverName) 144 | } 145 | } 146 | } 147 | } else if update.CallbackQuery != nil { 148 | callbackData := update.CallbackQuery.Data 149 | if len(callbackData) > 8 && callbackData[:8] == "/status " { 150 | serverName := callbackData[8:] 151 | // 编辑原消息并展示服务器信息 152 | sendOrEditServerStatus(bot, update.CallbackQuery.Message.Chat.ID, update.CallbackQuery.Message.MessageID, serverName) 153 | } 154 | } 155 | } 156 | } 157 | 158 | // 设置命令列表 159 | func setBotCommands(bot *tgbotapi.BotAPI) { 160 | commands := []tgbotapi.BotCommand{ 161 | {Command: "id", Description: "查看你的 Telegram 用户 ID"}, 162 | {Command: "akall", Description: "查看所有服务器的统计信息"}, 163 | {Command: "server", Description: "获取服务器列表"}, 164 | {Command: "status", Description: "查看某个服务器状态"}, 165 | } 166 | // 创建命令配置 167 | _, err := bot.Request(tgbotapi.NewSetMyCommands(commands...)) 168 | if err != nil { 169 | log.Println("设置命令失败:", err) 170 | } else { 171 | log.Println("命令设置成功") 172 | } 173 | } 174 | 175 | // 新建/编辑消息展示单个服务器信息 176 | func sendOrEditServerStatus(bot *tgbotapi.BotAPI, chatID int64, messageID int, serverName string) { 177 | var data Data 178 | if err := db.First(&data, "name = ?", serverName).Error; err != nil { 179 | // messageID != 0 说明消息来自点击列表中按钮回调, 只需要编辑原信息; 否则消息来自直接调用 /status 命令, 需要新建信息 180 | if messageID != 0 { 181 | editMsg := tgbotapi.NewEditMessageText(chatID, messageID, "未找到指定的服务器信息。") 182 | bot.Send(editMsg) 183 | } else { 184 | bot.Send(tgbotapi.NewMessage(chatID, "未找到指定的服务器信息。")) 185 | } 186 | return 187 | } 188 | 189 | var m M 190 | if err := json.Unmarshal([]byte(data.Data), &m); err != nil { 191 | // messageID != 0 说明消息来自点击列表中按钮回调, 只需要编辑原信息; 否则消息来自直接调用 /status 命令, 需要新建信息 192 | if messageID != 0 { 193 | editMsg := tgbotapi.NewEditMessageText(chatID, messageID, "解析服务器数据失败。") 194 | bot.Send(editMsg) 195 | } else { 196 | bot.Send(tgbotapi.NewMessage(chatID, "解析服务器数据失败。")) 197 | } 198 | return 199 | } 200 | 201 | msgText := formatServerMessage(serverName, &m) 202 | if messageID != 0 { 203 | editMsg := tgbotapi.NewEditMessageText(chatID, messageID, msgText) 204 | bot.Send(editMsg) 205 | } else { 206 | bot.Send(tgbotapi.NewMessage(chatID, msgText)) 207 | } 208 | } 209 | 210 | // 格式化服务器信息 211 | func formatServerMessage(serverName string, m *M) string { 212 | //流量对等性 213 | var duideng string 214 | if m.State.NetInTransfer > m.State.NetOutTransfer { 215 | duideng = fmt.Sprintf("%.2f%%", float64(m.State.NetOutTransfer)/float64(m.State.NetInTransfer)*100) 216 | } else { 217 | duideng = fmt.Sprintf("%.2f%%", float64(m.State.NetInTransfer)/float64(m.State.NetOutTransfer)*100) 218 | } 219 | return fmt.Sprintf(`服务器: %s 220 | CPU核心数: %d 221 | 内存: %s [%s/%s] 222 | 交换分区: %s [%s/%s] 223 | 下行速度: ↓%s/s 224 | 上行速度: ↑%s/s 225 | 下行流量: ↓%s 226 | 上行流量: ↑%s 227 | 流量对等性: %s 228 | 运行时间: %s 229 | 230 | 更新于: %s UTC`, 231 | serverName, 232 | parseCPU(m.Host.CPU[0]), 233 | fmt.Sprintf("%.2f%%", float64(m.State.MemUsed)/float64(m.Host.MemTotal)*100), 234 | formatSize(m.State.MemUsed), 235 | formatSize(m.Host.MemTotal), 236 | fmt.Sprintf("%.2f%%", float64(m.State.SwapUsed)/float64(m.Host.SwapTotal)*100), 237 | formatSize(m.State.SwapUsed), 238 | formatSize(m.Host.SwapTotal), 239 | formatSize(m.State.NetInSpeed), 240 | formatSize(m.State.NetOutSpeed), 241 | formatSize(m.State.NetInTransfer), 242 | formatSize(m.State.NetOutTransfer), 243 | duideng, 244 | time.Duration(m.State.Uptime)*time.Second, 245 | time.Now().UTC().Format("2006-01-02 15:04:05"), 246 | ) 247 | } 248 | 249 | // 解析CPU数量 250 | func parseCPU(cpu string) int { 251 | re := regexp.MustCompile(`(\d+) (Virtual) Core`) 252 | 253 | // 查找匹配项 254 | matches := re.FindStringSubmatch(cpu) 255 | if len(matches) >= 2 { 256 | virtualCores := matches[1] 257 | 258 | vint, err := strconv.Atoi(virtualCores) 259 | if err != nil { 260 | return 0 261 | } 262 | return vint 263 | } 264 | return 0 265 | } 266 | 267 | // 格式化字节大小 268 | func formatSize(size uint64) string { 269 | if size < 1024 { 270 | return fmt.Sprintf("%d B", size) 271 | } else if size < 1024*1024 { 272 | return fmt.Sprintf("%.2f KB", float64(size)/1024) 273 | } else if size < 1024*1024*1024 { 274 | return fmt.Sprintf("%.2f MB", float64(size)/1024/1024) 275 | } else if size < 1024*1024*1024*1024 { 276 | return fmt.Sprintf("%.2f GB", float64(size)/1024/1024/1024) 277 | } else if size < 1024*1024*1024*1024*1024 { 278 | return fmt.Sprintf("%.2f TB", float64(size)/1024/1024/1024/1024) 279 | } else if size < 1024*1024*1024*1024*1024*1024 { 280 | return fmt.Sprintf("%.2f PB", float64(size)/1024/1024/1024/1024/1024) 281 | } else { 282 | return fmt.Sprintf("%.2f EB", float64(size)/1024/1024/1024/1024/1024/1024) 283 | } 284 | } 285 | 286 | func SendTGMessage(msg string) { 287 | bot, err := tgbotapi.NewBotAPI(cfg.TgToken) 288 | if err != nil { 289 | log.Println("Error creating bot", err) 290 | return 291 | } 292 | 293 | bot.Debug = false 294 | 295 | log.Printf("Authorized on account %s", bot.Self.UserName) 296 | 297 | msgs := tgbotapi.NewMessage(cfg.TgChatID, msg) 298 | bot.Send(msgs) 299 | } 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andeya/goutil v1.0.1 h1:eiYwVyAnnK0dXU5FJsNjExkJW4exUGn/xefPt3k4eXg= 2 | github.com/andeya/goutil v1.0.1/go.mod h1:jEG5/QnnhG7yGxwFUX6Q+JGMif7sjdHmmNVjn7nhJDo= 3 | github.com/bytedance/go-tagexpr/v2 v2.9.2 h1:QySJaAIQgOEDQBLS3x9BxOWrnhqu5sQ+f6HaZIxD39I= 4 | github.com/bytedance/go-tagexpr/v2 v2.9.2/go.mod h1:5qsx05dYOiUXOUgnQ7w3Oz8BYs2qtM/bJokdLb79wRM= 5 | github.com/bytedance/gopkg v0.0.0-20220413063733-65bf48ffb3a7/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q= 6 | github.com/bytedance/gopkg v0.0.0-20240507064146-197ded923ae3/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= 7 | github.com/bytedance/gopkg v0.1.0 h1:aAxB7mm1qms4Wz4sp8e1AtKDOeFLtdqvGiUe7aonRJs= 8 | github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ= 9 | github.com/bytedance/mockey v1.2.1/go.mod h1:+Jm/fzWZAuhEDrPXVjDf/jLM2BlLXJkwk94zf2JZ3X4= 10 | github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= 11 | github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk= 12 | github.com/bytedance/sonic v1.3.5/go.mod h1:V973WhNhGmvHxW6nQmsHEfHaoU9F3zTF+93rH03hcUQ= 13 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 14 | github.com/bytedance/sonic v1.8.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 15 | github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls= 16 | github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 17 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 18 | github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= 19 | github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 20 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 21 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 22 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 23 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 24 | github.com/cloudwego/hertz v0.3.2/go.mod h1:hnv3B7eZ6kMv7CKFHT2OC4LU0mA4s5XPyu/SbixLcrU= 25 | github.com/cloudwego/hertz v0.6.2/go.mod h1:2em2hGREvCBawsTQcQxyWBGVlCeo+N1pp2q0HkkbwR0= 26 | github.com/cloudwego/hertz v0.9.3 h1:uajvLn6LjEPjUqN/ewUZtWoRQWa2es2XTELdqDlOYMw= 27 | github.com/cloudwego/hertz v0.9.3/go.mod h1:gGVUfJU/BOkJv/ZTzrw7FS7uy7171JeYIZvAyV3wS3o= 28 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 29 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 30 | github.com/cloudwego/netpoll v0.2.6/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= 31 | github.com/cloudwego/netpoll v0.3.1/go.mod h1:1T2WVuQ+MQw6h6DpE45MohSvDTKdy2DlzCx2KsnPI4E= 32 | github.com/cloudwego/netpoll v0.6.2 h1:+KdILv5ATJU+222wNNXpHapYaBeRvvL8qhJyhcxRxrQ= 33 | github.com/cloudwego/netpoll v0.6.2/go.mod h1:kaqvfZ70qd4T2WtIIpCOi5Cxyob8viEpzLhCrTrz3HM= 34 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 35 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 38 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 39 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 40 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 41 | github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= 42 | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= 43 | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= 44 | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= 45 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 46 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 47 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= 48 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= 49 | github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 50 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= 52 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 53 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 54 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 56 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 57 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 58 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 59 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= 60 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 61 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 62 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 63 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 64 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 65 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 66 | github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= 67 | github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk= 68 | github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4= 69 | github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ= 70 | github.com/henrylee2cn/goutil v1.0.1 h1:/ovGBt82ORDlRW0zmz01mN9yAgr0UxNB/eF1PgnrTY4= 71 | github.com/henrylee2cn/goutil v1.0.1/go.mod h1:I9qYeMYwdKC7UFXMECNzCEv0fYuolqLeBMqsmeG7IVo= 72 | github.com/hertz-contrib/cors v0.1.0 h1:PQ5mATygSMzTlYtfyMyHjobYoJeHKe2Qt3tcAOgbI6E= 73 | github.com/hertz-contrib/cors v0.1.0/go.mod h1:VPReoq+Rvu/lZOfpp5CcX3x4mpZUc3EpSXBcVDcbvOc= 74 | github.com/hertz-contrib/websocket v0.1.0 h1:9awGM2xzKJySbvnDrZMSNQcJEKjk7VYFMzt5VdPycFU= 75 | github.com/hertz-contrib/websocket v0.1.0/go.mod h1:VqcJq3L1S6dZlJqa3kY/0FeQKMxGWwijvWhEUNagLmo= 76 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 77 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 78 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 79 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 80 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 81 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 82 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 83 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 84 | github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= 85 | github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 86 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 87 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 88 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 89 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 90 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 91 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 92 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 93 | github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg= 94 | github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U= 95 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 96 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 97 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 98 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 99 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 100 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 101 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 102 | github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= 103 | github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= 104 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 105 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 106 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 107 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 108 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 109 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 110 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 111 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 112 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 114 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 115 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 116 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 117 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 118 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 119 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 120 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 121 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 122 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 123 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 124 | github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 125 | github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 126 | github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 127 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 128 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 129 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 130 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 131 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 132 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 133 | github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= 134 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 135 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 136 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 137 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 138 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 139 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 140 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 141 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 142 | golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= 143 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 144 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 145 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 146 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 147 | golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 148 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 149 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 150 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 157 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 159 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 161 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 162 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 163 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 164 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 165 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 166 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 167 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 168 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 169 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 171 | google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= 172 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 173 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 174 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 175 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 176 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 177 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 178 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 179 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 180 | modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 181 | modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 182 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 183 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 184 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 185 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 186 | modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= 187 | modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 188 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 189 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 190 | --------------------------------------------------------------------------------