This commit is contained in:
2026-03-12 15:01:48 +08:00
commit f27996dde0
20 changed files with 5450 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
# Node / Frontend
node_modules/
dist/
.vite/
# Logs
debug-logs/
*.log
# Environment files
.env
.env.local
.env.*.local
# Go build artifacts
*.exe
*.dll
*.so
*.dylib
*.test
*.out
# Local SSH profiles (often contain secrets)
mengyaconnect-backend/data/ssh/*.json
# IDE / OS
.idea/
.vscode/
.DS_Store
Thumbs.db

154
CLAUDE.md Normal file
View File

@@ -0,0 +1,154 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository layout (high level)
- `mengyaconnect-backend/` — Go (Gin) HTTP + WebSocket server that:
- exposes a WebSocket SSH “bridge” (`/api/ws/ssh`) backed by `golang.org/x/crypto/ssh`
- persists SSH profiles / quick commands / scripts to the local filesystem under `data/`
- `mengyaconnect-frontend/` — Vite + Vue 3 single-page UI using `xterm.js` to render terminals and talk to the backend WebSocket.
There is no top-level build system; run commands inside each subproject directory.
## Common development commands
### Backend (Go)
Run the server (default `:8080`):
```sh
cd mengyaconnect-backend
go run .
```
Build a local binary:
```sh
cd mengyaconnect-backend
go build -o mengyaconnect-backend
```
Basic checks (no dedicated lint/test tooling is configured beyond standard Go tools):
```sh
cd mengyaconnect-backend
go fmt ./...
go test ./...
# run a single Go test (if/when tests exist)
go test -run TestName ./...
```
### Frontend (Vite + Vue)
Install deps (repo includes `package-lock.json`):
```sh
cd mengyaconnect-frontend
npm install
```
Run dev server (default `http://localhost:5173`):
```sh
cd mengyaconnect-frontend
npm run dev
```
Build + preview:
```sh
cd mengyaconnect-frontend
npm run build
npm run preview
```
## Backend architecture (mengyaconnect-backend)
**Single-file server:** Almost all backend logic lives in `mengyaconnect-backend/main.go`.
### HTTP routes
Defined in `main.go` near the top:
- `GET /health` — basic health check.
- `GET /api/ws/ssh` — WebSocket endpoint for interactive SSH.
- SSH profile CRUD:
- `GET /api/ssh`
- `POST /api/ssh`
- `PUT /api/ssh/:name`
- `DELETE /api/ssh/:name`
- Quick command CRUD (stored as an array; updates by index):
- `GET /api/commands`
- `POST /api/commands`
- `PUT /api/commands/:index`
- `DELETE /api/commands/:index`
- Script CRUD (stored as files):
- `GET /api/scripts`
- `GET /api/scripts/:name`
- `POST /api/scripts`
- `PUT /api/scripts/:name`
- `DELETE /api/scripts/:name`
Response convention is typically `{ "data": ... }` on success and `{ "error": "..." }` on failure.
### Persistence model (filesystem)
All persisted state is stored under a base directory:
- base: `DATA_DIR` env var, default `data/`
- SSH profiles: `data/ssh/*.json`
- commands list: `data/command/command.json`
- scripts: `data/script/<name>`
`sanitizeName()` is used for path-safety (prevents `../` traversal by forcing `filepath.Base`).
Note: the repo currently contains example data files under `mengyaconnect-backend/data/` (including SSH profiles). Treat these as sensitive and rotate/remove before sharing the repository.
### WebSocket SSH bridge
The backend upgrades `/api/ws/ssh` and uses a simple JSON message protocol (`wsMessage` in `main.go`).
Client → server message types:
- `connect`: `{ host, port, username, password? | privateKey? , passphrase?, cols, rows }`
- `input`: `{ data }` (raw terminal input)
- `resize`: `{ cols, rows }`
- `ping`
- `close`
Server → client message types:
- `status`: `{ status, message }` (e.g. connected/ready/closing/closed)
- `output`: `{ data }` (stdout/stderr bytes as text)
- `error`: `{ message }`
- `pong`
SSH implementation notes:
- PTY is requested as `xterm-256color`.
- Host key verification is currently disabled via `ssh.InsecureIgnoreHostKey()`.
### Backend configuration (env)
- `PORT` (default `8080`) or `ADDR` (default `:<PORT>`)
- `DATA_DIR` (default `data`)
- `GIN_MODE` (if set, passed to `gin.SetMode`)
- `ALLOWED_ORIGINS` (comma-separated) is used by the WebSocket upgrader `CheckOrigin`.
## Frontend architecture (mengyaconnect-frontend)
**Single-screen UI:** Most logic is in `mengyaconnect-frontend/src/App.vue`.
- Uses `@xterm/xterm` + `@xterm/addon-fit`.
- Supports multiple concurrent sessions via tabs (each tab owns its own `WebSocket` + `Terminal`).
- Terminal input is forwarded to the backend as `{ type: "input", data }`.
- Resize events are forwarded as `{ type: "resize", cols, rows }`.
### Frontend configuration (Vite env)
`wsUrl` is computed in `App.vue`:
- If `VITE_WS_URL` is set, it is used as the full WebSocket URL.
- Otherwise it builds `${ws|wss}://${window.location.hostname}:${VITE_WS_PORT||8080}/api/ws/ssh`.
In development, youll usually run:
- backend on `localhost:8080`
- frontend on `localhost:5173`

View File

@@ -0,0 +1,33 @@
FROM golang:1.21-alpine AS builder
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o mengyaconnect-backend .
FROM alpine:3.19
WORKDIR /app
RUN apk add --no-cache ca-certificates && \
adduser -D -H appuser
COPY --from=builder /app/mengyaconnect-backend /app/mengyaconnect-backend
ENV DATA_DIR=/app/data
ENV PORT=8080
RUN mkdir -p "$DATA_DIR" && chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["/app/mengyaconnect-backend"]

View File

@@ -0,0 +1,78 @@
package main
import (
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// 数据目录辅助
func dataBasePath() string { return getEnv("DATA_DIR", "data") }
func sshDir() string { return filepath.Join(dataBasePath(), "ssh") }
func cmdFilePath() string { return filepath.Join(dataBasePath(), "command", "command.json") }
func scriptDir() string { return filepath.Join(dataBasePath(), "script") }
// sanitizeName 防止路径穿越攻击
func sanitizeName(name string) (string, error) {
base := filepath.Base(name)
if base == "" || base == "." || base == ".." {
return "", errors.New("invalid name")
}
return base, nil
}
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func isOriginAllowed(origin string, allowed []string) bool {
if origin == "" {
return true
}
if len(allowed) == 0 {
return true
}
for _, item := range allowed {
if item == "*" || strings.EqualFold(strings.TrimSpace(item), origin) {
return true
}
}
return false
}
func parseListEnv(name string) []string {
raw := strings.TrimSpace(os.Getenv(name))
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}
func getEnv(key, fallback string) string {
if val := strings.TrimSpace(os.Getenv(key)); val != "" {
return val
}
return fallback
}

View File

@@ -0,0 +1,22 @@
[
{
"alias": "安全关机",
"command": "shutdown -h now"
},
{
"alias": "定时十分钟后关机",
"command": "shutdown -h 10"
},
{
"alias": "重启",
"command": "reboot"
},
{
"alias": "输出当前目录",
"command": "pwd"
},
{
"alias": "列出当前目录文件",
"command": "ls -al"
}
]

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env bash
# =============================================================================
# Docker Info Collector - Single Column Edition
# =============================================================================
set -euo pipefail
# =============================================================================
# Colors
# =============================================================================
readonly NC='\033[0m'
readonly GRAY='\033[0;90m'
readonly CYAN='\033[0;36m'
readonly GREEN='\033[1;32m'
readonly YELLOW='\033[1;33m'
readonly WHITE='\033[1;37m'
readonly DIM='\033[2m'
readonly C_HEADER="${CYAN}"
readonly C_SECTION="${YELLOW}"
readonly C_KEY="${WHITE}"
readonly C_VALUE="${CYAN}"
readonly C_OK="${GREEN}"
readonly C_WARN="${YELLOW}"
readonly C_ERR="\033[1;31m"
readonly C_DIM="${GRAY}"
# Separator line (thick line style)
SEP_LINE="${C_DIM}══════════════════════════════════════════════════════════════════════════════${NC}"
# =============================================================================
# Utils
# =============================================================================
has_cmd() { command -v "$1" &>/dev/null; }
print_header() {
local title="$1"
local len=${#title}
local pad=$(( (76 - len) / 2 ))
echo -e "\n${C_HEADER}╔══════════════════════════════════════════════════════════════════════════════╗"
printf "${C_HEADER}║%${pad}s${WHITE} %s %${pad}s║${NC}\n" "" "$title" ""
echo -e "${C_HEADER}╚══════════════════════════════════════════════════════════════════════════════╝${NC}"
}
print_section() {
echo -e "\n${C_SECTION}$1${NC}"
echo -e "$SEP_LINE"
}
print_kv() {
printf " ${C_KEY}%-14s${NC} ${C_VALUE}%s${NC}\n" "$1:" "$2"
}
print_ok() { echo -e " ${C_OK}${NC} $1"; }
print_warn() { echo -e " ${C_WARN}${NC} $1"; }
print_err() { echo -e " ${C_ERR}${NC} $1"; }
# =============================================================================
# System Info
# =============================================================================
collect_server() {
print_header "服务器信息"
# OS Information
print_section "操作系统"
local os_name kernel arch
os_name="$(. /etc/os-release 2>/dev/null && echo "$PRETTY_NAME" || uname -s)"
kernel="$(uname -r)"
arch="$(uname -m)"
print_kv "系统" "$os_name"
print_kv "内核版本" "$kernel"
print_kv "系统架构" "$arch"
# Host Info
print_section "主机信息"
local hostname uptime_str
hostname="$(hostname)"
uptime_str="$(uptime -p 2>/dev/null | sed 's/up //' || uptime | sed 's/.*up //; s/,.*//')"
print_kv "主机名" "$hostname"
print_kv "运行时长" "$uptime_str"
print_kv "当前时间" "$(date '+%Y-%m-%d %H:%M:%S')"
# CPU
if [[ -f /proc/cpuinfo ]]; then
print_section "处理器"
local cpus cpu_model
cpus=$(grep -c '^processor' /proc/cpuinfo)
cpu_model=$(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)
print_kv "核心数量" "${cpus}"
print_kv "型号" "$cpu_model"
fi
# Memory
if has_cmd free; then
print_section "内存"
local mem_total mem_used mem_free mem_pct
mem_total=$(free -h | awk '/^Mem:/ {print $2}')
mem_used=$(free -h | awk '/^Mem:/ {print $3}')
mem_free=$(free -h | awk '/^Mem:/ {print $7}')
mem_pct=$(free | awk '/^Mem:/ {printf "%.1f", $3/$2 * 100}')
print_kv "总容量" "$mem_total"
print_kv "已使用" "$mem_used (${mem_pct}%)"
print_kv "可用" "$mem_free"
fi
# Disk
if has_cmd df; then
print_section "磁盘使用"
df -h --output=source,fstype,size,used,pcent,target 2>/dev/null | tail -n +2 | while read -r fs type size used pct target; do
[[ "$fs" == "tmpfs" || "$fs" == "devtmpfs" || "$fs" == "overlay" ]] && continue
[[ -z "$fs" ]] && continue
# Escape % for printf
local pct_clean="${pct%%%}"
printf " ${C_DIM}[${NC}${C_VALUE}%-12s${NC}${C_DIM}]${NC} ${C_KEY}%-8s${NC} ${C_VALUE}%-7s${NC} ${C_DIM}used:${NC} ${C_VALUE}%-7s${NC} ${C_DIM}(%s%%)${NC}\n" \
"$target" "$type" "$size" "$used" "$pct_clean"
done
fi
# Network
if has_cmd ip; then
print_section "网络接口"
ip -o addr show 2>/dev/null | awk '{print $2, $4}' | while read -r iface addr; do
[[ "$iface" == "lo" ]] && continue
print_kv "$iface" "$addr"
done
fi
}
# =============================================================================
# Docker Info
# =============================================================================
collect_docker() {
print_header "Docker 信息"
if ! has_cmd docker; then
print_err "Docker 未安装"
return 1
fi
# Version
print_section "版本信息"
local client_ver server_ver
client_ver=$(docker version --format '{{.Client.Version}}' 2>/dev/null || echo "N/A")
server_ver=$(docker version --format '{{.Server.Version}}' 2>/dev/null || echo "N/A")
print_kv "客户端" "$client_ver"
print_kv "服务端" "$server_ver"
if has_cmd docker-compose; then
print_kv "Docker Compose" "$(docker-compose version --short 2>/dev/null)"
fi
# Status
if docker info &>/dev/null; then
print_ok "守护进程运行中"
else
print_warn "守护进程未运行"
return 1
fi
# Stats
print_section "资源统计"
local containers running images networks volumes
containers=$(docker ps -aq 2>/dev/null | wc -l)
running=$(docker ps -q 2>/dev/null | wc -l)
images=$(docker images -q 2>/dev/null | wc -l)
networks=$(docker network ls -q 2>/dev/null | wc -l)
volumes=$(docker volume ls -q 2>/dev/null | wc -l)
print_kv "容器" "${running} 运行 / ${containers} 总计"
print_kv "镜像" "$images"
print_kv "网络" "$networks"
print_kv "存储卷" "$volumes"
# Running containers
if [[ $running -gt 0 ]]; then
print_section "运行中的容器"
docker ps --format "{{.Names}}|{{.Image}}|{{.Status}}" 2>/dev/null | while IFS='|' read -r name image status; do
printf " ${C_OK}${NC} ${C_VALUE}%-20s${NC} ${C_DIM}%-30s${NC} %s\n" "$name" "${image:0:30}" "$status"
done
fi
# All containers
if [[ $containers -gt 0 ]]; then
print_section "所有容器"
docker ps -a --format "{{.Names}}|{{.Image}}|{{.Status}}" 2>/dev/null | head -20 | while IFS='|' read -r name image status; do
local icon="${C_DIM}${NC}"
local color="$C_VALUE"
[[ "$status" == Up* ]] && icon="${C_OK}${NC}" && color="$C_OK"
[[ "$status" == Exited* ]] && color="$C_DIM"
printf " ${icon} ${color}%-20s${NC} ${C_DIM}%-25s${NC} %s\n" "$name" "${image:0:25}" "$status"
done
[[ $containers -gt 20 ]] && echo -e " ${C_DIM}... 还有 $((containers - 20)) 个容器${NC}"
fi
# Images
if [[ $images -gt 0 ]]; then
print_section "镜像列表"
docker images --format "{{.Repository}}|{{.Tag}}|{{.Size}}|{{.CreatedAt}}" 2>/dev/null | grep -v "<none>" | head -25 | while IFS='|' read -r repo tag size created; do
printf " ${C_DIM}${NC} ${C_VALUE}%-35s${NC} ${C_DIM}%-10s %s${NC}\n" "${repo}:${tag}" "$size" "$created"
done
[[ $images -gt 25 ]] && echo -e " ${C_DIM}... 还有 $((images - 25)) 个镜像${NC}"
fi
# Networks
if [[ $networks -gt 0 ]]; then
print_section "网络"
docker network ls --format "{{.Name}}|{{.Driver}}|{{.Scope}}" 2>/dev/null | head -15 | while IFS='|' read -r name driver scope; do
printf " ${C_DIM}${NC} ${C_VALUE}%-20s${NC} ${C_DIM}[%s/%s]${NC}\n" "$name" "$driver" "$scope"
done
fi
# Volumes
if [[ $volumes -gt 0 ]]; then
print_section "存储卷"
docker volume ls --format "{{.Name}}|{{.Driver}}" 2>/dev/null | head -20 | while IFS='|' read -r name driver; do
printf " ${C_DIM}${NC} ${C_VALUE}%s${NC} ${C_DIM}(%s)${NC}\n" "$name" "$driver"
done
[[ $volumes -gt 20 ]] && echo -e " ${C_DIM}... 还有 $((volumes - 20)) 个存储卷${NC}"
fi
}
# =============================================================================
# Main
# =============================================================================
main() {
collect_server
collect_docker
echo -e "\n${C_HEADER}╔══════════════════════════════════════════════════════════════════════════════╗"
echo -e "${C_HEADER}${C_OK} ✓ 信息收集完成 ${C_HEADER}${NC}"
echo -e "${C_HEADER}╚══════════════════════════════════════════════════════════════════════════════╝${NC}\n"
}
main "$@"

View File

@@ -0,0 +1,802 @@
#!/bin/bash
# systemctl-info - 详细的systemctl信息查看脚本
# 作者: iFlow CLI
# 日期: 2026-02-13
# 版本: 2.0 (模块化版本)
# ═══════════════════════════════════════════════════════════════════════════════
# 颜色定义
# ═══════════════════════════════════════════════════════════════════════════════
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[0;37m'
BRIGHT_RED='\033[1;31m'
BRIGHT_GREEN='\033[1;32m'
BRIGHT_YELLOW='\033[1;33m'
BRIGHT_BLUE='\033[1;34m'
BRIGHT_MAGENTA='\033[1;35m'
BRIGHT_CYAN='\033[1;36m'
BRIGHT_WHITE='\033[1;37m'
ORANGE='\033[38;5;208m'
PINK='\033[38;5;205m'
PURPLE='\033[38;5;141m'
LIME='\033[38;5;154m'
RESET='\033[0m'
# ═══════════════════════════════════════════════════════════════════════════════
# 分割线样式
# ═══════════════════════════════════════════════════════════════════════════════
SEPARATOR="${BRIGHT_CYAN}═══════════════════════════════════════════════════════════════════════════════${RESET}"
THIN_SEPARATOR="${CYAN}──────────────────────────────────────────────────────────────────────────────────────${RESET}"
DASH_SEPARATOR="${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
LINE="${BRIGHT_CYAN}═══════════════════════════════════════════════════════════════════════════════${RESET}"
# ═══════════════════════════════════════════════════════════════════════════════
# 通用工具函数
# ═══════════════════════════════════════════════════════════════════════════════
# 打印标题
print_header() {
echo -e "${BRIGHT_CYAN}[Systemctl Info Viewer v2.0]${RESET}"
echo -e "${BRIGHT_CYAN}模块化 Systemd 信息查看脚本${RESET}"
echo ""
}
# 打印带颜色的小节标题
print_section() {
echo -e "${THIN_SEPARATOR}"
echo -e "${BRIGHT_BLUE}[ $1 ]${RESET}"
echo -e "${THIN_SEPARATOR}"
}
# 打印带图标的信息行
print_info() {
local icon="$1"
local label="$2"
local value="$3"
local color="$4"
echo -e "${icon} ${BRIGHT_WHITE}${label}:${RESET} ${color}${value}${RESET}"
}
# 打印子项
print_subitem() {
local label="$1"
local value="$2"
local color="$3"
echo -e " ${BRIGHT_CYAN}${RESET} ${BRIGHT_WHITE}${label}:${RESET} ${color}${value}${RESET}"
}
# 打印带颜色的列表项
print_list_item() {
local icon="$1"
local name="$2"
local status="$3"
local status_color="$4"
local extra="$5"
printf "${icon} ${BRIGHT_WHITE}%-45s${RESET} ${status_color}%s${RESET}%s\n" "$name" "$status" "$extra"
}
# 获取状态图标和颜色
get_state_icon_color() {
local state="$1"
case "$state" in
active|running|listening)
echo -e "✅|${BRIGHT_GREEN}"
;;
inactive)
echo -e "⭕|${BRIGHT_YELLOW}"
;;
failed)
echo -e "❌|${BRIGHT_RED}"
;;
activating|deactivating)
echo -e "🔄|${BRIGHT_CYAN}"
;;
*)
echo -e "❓|${BRIGHT_WHITE}"
;;
esac
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块1: Systemd 版本信息
# ═══════════════════════════════════════════════════════════════════════════════
module_systemd_version() {
print_section "📋 Systemd 版本信息"
SYSTEMD_VERSION=$(systemctl --version | head -n 1 | awk '{print $2}')
FEATURE_COUNT=$(systemctl --version | grep -c "features")
print_info "🔧" "Systemd 版本" "$SYSTEMD_VERSION" "${BRIGHT_GREEN}"
print_info "✨" "支持功能特性" "$FEATURE_COUNT" "${LIME}"
echo -e "${BRIGHT_CYAN}详细版本信息:${RESET}"
systemctl --version | while IFS= read -r line; do
echo -e " ${CYAN}${line}${RESET}"
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块2: 系统基础信息
# ═══════════════════════════════════════════════════════════════════════════════
module_system_info() {
print_section "🖥️ 系统基础信息"
HOSTNAME=$(hostname)
KERNEL_VERSION=$(uname -r)
OS_ID=$(grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '"')
OS_NAME=$(grep '^PRETTY_NAME=' /etc/os-release | cut -d'=' -f2 | tr -d '"')
ARCH=$(uname -m)
UPTIME=$(uptime -p)
print_info "🖥️" "主机名" "$HOSTNAME" "${BRIGHT_YELLOW}"
print_info "🐧" "内核版本" "$KERNEL_VERSION" "${ORANGE}"
print_info "📦" "操作系统ID" "$OS_ID" "${PINK}"
print_info "💻" "系统名称" "$OS_NAME" "${PURPLE}"
print_info "🏗️" "系统架构" "$ARCH" "${BRIGHT_CYAN}"
print_info "⏱️" "系统运行时间" "$UPTIME" "${LIME}"
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块3: Systemd 系统状态
# ═══════════════════════════════════════════════════════════════════════════════
module_systemd_status() {
print_section "⚙️ Systemd 系统状态"
SYSTEM_STATE=$(systemctl is-system-running)
DEFAULT_TARGET=$(systemctl get-default)
INIT_PID=$(systemctl show --property=MainPID --value)
BOOT_TIME=$(systemctl show --property=UserspaceTimestamp --value | cut -d' ' -f1)
case $SYSTEM_STATE in
running)
STATE_COLOR="${BRIGHT_GREEN}"
STATE_ICON="✅"
;;
degraded)
STATE_COLOR="${BRIGHT_YELLOW}"
STATE_ICON="⚠️"
;;
maintenance)
STATE_COLOR="${BRIGHT_RED}"
STATE_ICON="🔧"
;;
*)
STATE_COLOR="${BRIGHT_WHITE}"
STATE_ICON="❓"
;;
esac
print_info "$STATE_ICON" "系统状态" "$SYSTEM_STATE" "$STATE_COLOR"
print_info "🎯" "默认运行级别" "$DEFAULT_TARGET" "${BRIGHT_CYAN}"
print_info "🔄" "Init 进程 PID" "$INIT_PID" "${ORANGE}"
print_info "🚀" "启动时间" "$BOOT_TIME" "${PURPLE}"
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块4: 服务(Service)统计与状态
# ═══════════════════════════════════════════════════════════════════════════════
module_service_stats() {
print_section "🔌 服务(Service)统计与状态"
TOTAL_UNITS=$(systemctl list-unit-files --type=service --no-legend | wc -l)
ENABLED_SERVICES=$(systemctl list-unit-files --type=service --state=enabled --no-legend | wc -l)
DISABLED_SERVICES=$(systemctl list-unit-files --type=service --state=disabled --no-legend | wc -l)
STATIC_SERVICES=$(systemctl list-unit-files --type=service --state=static --no-legend | wc -l)
MASKED_SERVICES=$(systemctl list-unit-files --type=service --state=masked --no-legend | wc -l)
RUNNING_SERVICES=$(systemctl list-units --type=service --state=running --no-legend | wc -l)
FAILED_SERVICES=$(systemctl list-units --type=service --state=failed --no-legend | wc -l)
echo -e "${BRIGHT_CYAN}服务文件统计:${RESET}"
print_subitem "总服务数" "$TOTAL_UNITS" "${BRIGHT_WHITE}"
print_subitem "已启用(enabled)" "$ENABLED_SERVICES" "${BRIGHT_GREEN}"
print_subitem "已禁用(disabled)" "$DISABLED_SERVICES" "${BRIGHT_RED}"
print_subitem "静态服务(static)" "$STATIC_SERVICES" "${BRIGHT_YELLOW}"
print_subitem "已屏蔽(masked)" "$MASKED_SERVICES" "${PURPLE}"
echo -e "${BRIGHT_CYAN}服务运行状态:${RESET}"
print_subitem "运行中" "$RUNNING_SERVICES" "${LIME}"
print_subitem "失败" "$FAILED_SERVICES" "${BRIGHT_RED}"
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块5: 失败的服务详情
# ═══════════════════════════════════════════════════════════════════════════════
module_failed_services() {
FAILED_SERVICES=$(systemctl list-units --type=service --state=failed --no-legend | wc -l)
if [ "$FAILED_SERVICES" -gt 0 ]; then
print_section "❌ 失败的服务详情 (共 $FAILED_SERVICES 个)"
systemctl list-units --type=service --state=failed --no-pager | sed '1,1d' | while IFS= read -r line; do
if [ -n "$line" ]; then
SERVICE_NAME=$(echo "$line" | awk '{print $1}')
LOAD_STATE=$(echo "$line" | awk '{print $2}')
ACTIVE_STATE=$(echo "$line" | awk '{print $3}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
DESCRIPTION=$(echo "$line" | awk '{for(i=5;i<=NF;i++)print $i}' | tr '\n' ' ' | sed 's/ $//')
echo -e "${BRIGHT_RED}${RESET} ${BRIGHT_WHITE}${SERVICE_NAME}${RESET}"
echo -e " ${CYAN}描述:${RESET} ${WHITE}${DESCRIPTION}${RESET}"
echo -e " ${CYAN}状态:${RESET} ${RED}${LOAD_STATE}${RESET}|${RED}${ACTIVE_STATE}${RESET}|${RED}${SUB_STATE}${RESET}"
fi
done
else
print_section "✅ 失败的服务详情"
echo -e "${BRIGHT_GREEN} 没有失败的服务${RESET}"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块6: 已屏蔽(Masked)的服务
# ═══════════════════════════════════════════════════════════════════════════════
module_masked_services() {
MASKED_COUNT=$(systemctl list-unit-files --type=service --state=masked --no-legend | wc -l)
print_section "🚫 已屏蔽(Masked)的服务"
print_subitem "已屏蔽服务数" "$MASKED_COUNT" "${PURPLE}"
if [ "$MASKED_COUNT" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}已屏蔽的服务列表:${RESET}"
systemctl list-unit-files --type=service --state=masked --no-legend | while IFS= read -r line; do
if [ -n "$line" ]; then
echo -e " ${PURPLE}${RESET} ${BRIGHT_WHITE}${line}${RESET}"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块7: 运行中的服务
# ═══════════════════════════════════════════════════════════════════════════════
module_running_services() {
print_section "🟢 运行中的服务 (Top 20)"
RUNNING_COUNT=$(systemctl list-units --type=service --state=running --no-legend | wc -l)
print_subitem "运行中服务总数" "$RUNNING_COUNT" "${LIME}"
echo -e "${BRIGHT_CYAN}运行中的服务列表:${RESET}"
systemctl list-units --type=service --state=running --no-pager --no-legend | head -20 | while IFS= read -r line; do
if [ -n "$line" ]; then
SERVICE_NAME=$(echo "$line" | awk '{print $1}')
LOAD_STATE=$(echo "$line" | awk '{print $2}')
ACTIVE_STATE=$(echo "$line" | awk '{print $3}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
printf " ${BRIGHT_GREEN}${RESET} ${BRIGHT_WHITE}%-40s${RESET} ${CYAN}%-8s${RESET} ${BRIGHT_GREEN}%-10s${RESET} ${BRIGHT_CYAN}%s${RESET}\n" "$SERVICE_NAME" "$LOAD_STATE" "$ACTIVE_STATE" "$SUB_STATE"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块8: Timer 定时任务
# ═══════════════════════════════════════════════════════════════════════════════
module_timer() {
print_section "⏰ Timer 定时任务"
TOTAL_TIMERS=$(systemctl list-units --type=timer --all --no-legend | wc -l)
ACTIVE_TIMERS=$(systemctl list-units --type=timer --state=active --no-legend | wc -l)
print_subitem "总 Timer 数" "$TOTAL_TIMERS" "${BRIGHT_WHITE}"
print_subitem "活跃 Timer" "$ACTIVE_TIMERS" "${LIME}"
if [ "$ACTIVE_TIMERS" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}活跃的定时任务 (Top 15):${RESET}"
systemctl list-units --type=timer --state=active --no-pager --no-legend | head -15 | while IFS= read -r line; do
if [ -n "$line" ]; then
TIMER_NAME=$(echo "$line" | awk '{print $1}')
NEXT_RUN=$(systemctl show "$TIMER_NAME" --property=NextElapseUSec --value 2>/dev/null)
printf " ${BRIGHT_YELLOW}${RESET} ${BRIGHT_WHITE}%-40s${RESET} ${CYAN}下次执行:${RESET} ${LIME}%s${RESET}\n" "$TIMER_NAME" "$NEXT_RUN"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块9: Socket 单元
# ═══════════════════════════════════════════════════════════════════════════════
module_socket() {
print_section "🔌 Socket 监听"
TOTAL_SOCKETS=$(systemctl list-units --type=socket --all --no-legend | wc -l)
LISTENING_SOCKETS=$(systemctl list-units --type=socket --state=listening --no-legend | wc -l)
print_subitem "总 Socket 数" "$TOTAL_SOCKETS" "${BRIGHT_WHITE}"
print_subitem "监听中" "$LISTENING_SOCKETS" "${LIME}"
if [ "$LISTENING_SOCKETS" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}正在监听的 Socket (Top 15):${RESET}"
systemctl list-units --type=socket --state=listening --no-pager --no-legend | head -15 | while IFS= read -r line; do
if [ -n "$line" ]; then
SOCKET_NAME=$(echo "$line" | awk '{print $1}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
printf " ${BRIGHT_MAGENTA}🔌${RESET} ${BRIGHT_WHITE}%-40s${RESET} ${CYAN}%s${RESET}\n" "$SOCKET_NAME" "$SUB_STATE"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块10: Target 目标单元
# ═══════════════════════════════════════════════════════════════════════════════
module_target() {
print_section "🎯 Target 目标单元"
ACTIVE_TARGETS=$(systemctl list-units --type=target --state=active --no-legend | wc -l)
print_subitem "当前激活的 Target" "$ACTIVE_TARGETS" "${LIME}"
echo -e "${BRIGHT_CYAN}重要的 Target 单元:${RESET}"
IMPORTANT_TARGETS=("default.target" "multi-user.target" "graphical.target" "basic.target" "rescue.target" "emergency.target" "network.target" "sysinit.target")
for target in "${IMPORTANT_TARGETS[@]}"; do
TARGET_STATE=$(systemctl is-active "$target" 2>/dev/null)
TARGET_ENABLED=$(systemctl is-enabled "$target" 2>/dev/null)
if [ -n "$TARGET_STATE" ]; then
case $TARGET_STATE in
active) STATE_ICON="✅"; STATE_COLOR="${BRIGHT_GREEN}" ;;
inactive) STATE_ICON="⭕"; STATE_COLOR="${BRIGHT_YELLOW}" ;;
*) STATE_ICON="❓"; STATE_COLOR="${BRIGHT_WHITE}" ;;
esac
printf " ${STATE_ICON} ${BRIGHT_WHITE}%-25s${RESET} ${STATE_COLOR}状态:%-10s${RESET} ${CYAN}启用:%s${RESET}\n" "$target" "$TARGET_STATE" "$TARGET_ENABLED"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块11: Mount 和 Automount 单元
# ═══════════════════════════════════════════════════════════════════════════════
module_mount() {
print_section "📁 挂载点(Mount)信息"
TOTAL_MOUNTS=$(systemctl list-units --type=mount --all --no-legend | wc -l)
ACTIVE_MOUNTS=$(systemctl list-units --type=mount --state=active --no-legend | wc -l)
TOTAL_AUTOMOUNTS=$(systemctl list-units --type=automount --all --no-legend | wc -l)
ACTIVE_AUTOMOUNTS=$(systemctl list-units --type=automount --state=active --no-legend | wc -l)
print_subitem "挂载点总数" "$TOTAL_MOUNTS" "${BRIGHT_WHITE}"
print_subitem "活跃挂载点" "$ACTIVE_MOUNTS" "${LIME}"
print_subitem "自动挂载总数" "$TOTAL_AUTOMOUNTS" "${BRIGHT_WHITE}"
print_subitem "活跃自动挂载" "$ACTIVE_AUTOMOUNTS" "${LIME}"
if [ "$ACTIVE_MOUNTS" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}挂载点详情 (Top 10):${RESET}"
systemctl list-units --type=mount --state=active --no-pager --no-legend | head -10 | while IFS= read -r line; do
if [ -n "$line" ]; then
MOUNT_NAME=$(echo "$line" | awk '{print $1}')
MOUNT_POINT=$(systemctl show "$MOUNT_NAME" --property=Where --value 2>/dev/null)
SUB_STATE=$(echo "$line" | awk '{print $4}')
printf " ${BRIGHT_CYAN}📂${RESET} ${BRIGHT_WHITE}%-35s${RESET} ${PURPLE}%s${RESET}\n" "$MOUNT_POINT" "$SUB_STATE"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块12: Path 单元
# ═══════════════════════════════════════════════════════════════════════════════
module_path() {
print_section "📍 Path 路径监控单元"
TOTAL_PATHS=$(systemctl list-units --type=path --all --no-legend | wc -l)
ACTIVE_PATHS=$(systemctl list-units --type=path --state=active --no-legend | wc -l)
print_subitem "总 Path 数" "$TOTAL_PATHS" "${BRIGHT_WHITE}"
print_subitem "活跃 Path" "$ACTIVE_PATHS" "${LIME}"
if [ "$ACTIVE_PATHS" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}活跃的 Path 监控 (Top 10):${RESET}"
systemctl list-units --type=path --state=active --no-pager --no-legend | head -10 | while IFS= read -r line; do
if [ -n "$line" ]; then
PATH_NAME=$(echo "$line" | awk '{print $1}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
PATH_PATH=$(systemctl show "$PATH_NAME" --property=PathExists --value 2>/dev/null)
printf " ${BRIGHT_CYAN}📍${RESET} ${BRIGHT_WHITE}%-40s${RESET} ${CYAN}监控:${RESET} ${LIME}%s${RESET} ${CYAN}状态:%s${RESET}\n" "$PATH_NAME" "$PATH_PATH" "$SUB_STATE"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块13: Device 单元
# ═══════════════════════════════════════════════════════════════════════════════
module_device() {
print_section "🔧 Device 设备单元"
TOTAL_DEVICES=$(systemctl list-units --type=device --all --no-legend | wc -l)
ACTIVE_DEVICES=$(systemctl list-units --type=device --state=active --no-legend | wc -l)
print_subitem "总设备数" "$TOTAL_DEVICES" "${BRIGHT_WHITE}"
print_subitem "活跃设备" "$ACTIVE_DEVICES" "${LIME}"
if [ "$ACTIVE_DEVICES" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}活跃的设备 (Top 10):${RESET}"
systemctl list-units --type=device --state=active --no-pager --no-legend | head -10 | while IFS= read -r line; do
if [ -n "$line" ]; then
DEVICE_NAME=$(echo "$line" | awk '{print $1}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
printf " ${BRIGHT_YELLOW}🔧${RESET} ${BRIGHT_WHITE}%-45s${RESET} ${LIME}%s${RESET}\n" "$DEVICE_NAME" "$SUB_STATE"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块14: Scope 和 Slice 单元
# ═══════════════════════════════════════════════════════════════════════════════
module_scope_slice() {
print_section "📊 Scope 和 Slice 资源控制单元"
# Scope 单元
TOTAL_SCOPES=$(systemctl list-units --type=scope --all --no-legend | wc -l)
ACTIVE_SCOPES=$(systemctl list-units --type=scope --state=running --no-legend | wc -l)
print_subitem "Scope 总数" "$TOTAL_SCOPES" "${BRIGHT_WHITE}"
print_subitem "运行中 Scope" "$ACTIVE_SCOPES" "${LIME}"
if [ "$ACTIVE_SCOPES" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}运行中的 Scope (Top 10):${RESET}"
systemctl list-units --type=scope --state=running --no-pager --no-legend | head -10 | while IFS= read -r line; do
if [ -n "$line" ]; then
SCOPE_NAME=$(echo "$line" | awk '{print $1}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
printf " ${BRIGHT_CYAN}📊${RESET} ${BRIGHT_WHITE}%-45s${RESET} ${LIME}%s${RESET}\n" "$SCOPE_NAME" "$SUB_STATE"
fi
done
fi
# Slice 单元
echo ""
TOTAL_SLICES=$(systemctl list-units --type=slice --all --no-legend | wc -l)
ACTIVE_SLICES=$(systemctl list-units --type=slice --state=active --no-legend | wc -l)
print_subitem "Slice 总数" "$TOTAL_SLICES" "${BRIGHT_WHITE}"
print_subitem "活跃 Slice" "$ACTIVE_SLICES" "${LIME}"
if [ "$ACTIVE_SLICES" -gt 0 ]; then
echo -e "${BRIGHT_CYAN}活跃的 Slice:${RESET}"
systemctl list-units --type=slice --state=active --no-pager --no-legend | head -10 | while IFS= read -r line; do
if [ -n "$line" ]; then
SLICE_NAME=$(echo "$line" | awk '{print $1}')
SUB_STATE=$(echo "$line" | awk '{print $4}')
MEMORY=$(systemctl show "$SLICE_NAME" --property=MemoryCurrent --value 2>/dev/null)
if [ -n "$MEMORY" ] && [ "$MEMORY" != "[not set]" ]; then
MEMORY_DISPLAY="内存: $(numfmt --to=iec $MEMORY 2>/dev/null || echo $MEMORY)"
else
MEMORY_DISPLAY=""
fi
printf " ${BRIGHT_MAGENTA}📦${RESET} ${BRIGHT_WHITE}%-30s${RESET} ${LIME}%s${RESET} %s\n" "$SLICE_NAME" "$SUB_STATE" "$MEMORY_DISPLAY"
fi
done
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块15: 依赖关系
# ═══════════════════════════════════════════════════════════════════════════════
module_dependencies() {
print_section "🔗 系统依赖关系"
# 显示默认.target的依赖树
DEFAULT_TARGET=$(systemctl get-default)
echo -e "${BRIGHT_CYAN}默认目标 '$DEFAULT_TARGET' 的依赖 (前15个):${RESET}"
systemctl list-dependencies "$DEFAULT_TARGET" --no-pager --no-legend | head -15 | while IFS= read -r line; do
if [ -n "$line" ]; then
UNIT_TYPE=$(echo "$line" | grep -o '\.[a-z]*$' | tr -d '.')
case "$UNIT_TYPE" in
service) ICON="🔌" ;;
target) ICON="🎯" ;;
timer) ICON="⏰" ;;
socket) ICON="🔌" ;;
mount) ICON="📁" ;;
path) ICON="📍" ;;
*) ICON="📄" ;;
esac
printf " ${ICON} ${BRIGHT_WHITE}%s${RESET}\n" "$line"
fi
done
# 显示被依赖最多的服务
echo ""
echo -e "${BRIGHT_CYAN}系统关键.target的依赖数量:${RESET}"
for target in "multi-user.target" "graphical.target" "basic.target" "network.target"; do
DEP_COUNT=$(systemctl list-dependencies "$target" --no-legend 2>/dev/null | wc -l)
if [ -n "$DEP_COUNT" ]; then
print_subitem "$target" "$DEP_COUNT 个依赖" "${BRIGHT_CYAN}"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块16: Systemd 日志信息
# ═══════════════════════════════════════════════════════════════════════════════
module_journal() {
print_section "📝 Systemd Journal 日志摘要"
JOURNAL_SIZE=$(journalctl --disk-usage | grep "Journals use" | awk '{print $3,$4}')
JOURNAL_ENTRIES=$(journalctl --no-pager -n 0 2>/dev/null | wc -l)
print_subitem "日志磁盘占用" "$JOURNAL_SIZE" "${ORANGE}"
print_subitem "日志总条目" "$JOURNAL_ENTRIES" "${BRIGHT_CYAN}"
echo -e "${BRIGHT_CYAN}最近的错误日志 (最近5条):${RESET}"
journalctl -p err -n 5 --no-pager 2>/dev/null | while IFS= read -r line; do
if [ -n "$line" ]; then
echo -e " ${RED}${RESET} ${WHITE}${line}${RESET}"
fi
done
echo -e "${BRIGHT_CYAN}最近的警告日志 (最近3条):${RESET}"
journalctl -p warning -n 3 --no-pager 2>/dev/null | while IFS= read -r line; do
if [ -n "$line" ]; then
echo -e " ${YELLOW}${RESET} ${WHITE}${line}${RESET}"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块17: Systemd 环境变量
# ═══════════════════════════════════════════════════════════════════════════════
module_environment() {
print_section "🔧 Systemd 环境变量"
ENV_COUNT=$(systemctl show-environment 2>/dev/null | wc -l)
print_subitem "环境变量数量" "$ENV_COUNT" "${BRIGHT_CYAN}"
echo -e "${BRIGHT_CYAN}系统环境变量:${RESET}"
systemctl show-environment 2>/dev/null | head -15 | while IFS= read -r line; do
if [ -n "$line" ]; then
KEY=$(echo "$line" | cut -d'=' -f1)
VALUE=$(echo "$line" | cut -d'=' -f2-)
echo -e " ${BRIGHT_YELLOW}${RESET} ${BRIGHT_CYAN}${KEY}${RESET}=${BRIGHT_WHITE}${VALUE}${RESET}"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块18: Cgroup 信息
# ═══════════════════════════════════════════════════════════════════════════════
module_cgroup() {
print_section "🧊 Cgroup 信息"
# 获取 cgroup 版本
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
CGROUP_VERSION="v2 (unified)"
else
CGROUP_VERSION="v1 (legacy)"
fi
print_subitem "Cgroup 版本" "$CGROUP_VERSION" "${BRIGHT_CYAN}"
# 获取控制器信息
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
CONTROLLERS=$(cat /sys/fs/cgroup/cgroup.controllers 2>/dev/null | tr ' ' ', ')
else
CONTROLLERS=$(cat /sys/fs/cgroup/devices.list 2>/dev/null | head -1 | cut -d' ' -f1 || echo "N/A")
fi
print_subitem "可用控制器" "$CONTROLLERS" "${LIME}"
# Slice 资源统计
echo -e "${BRIGHT_CYAN}Slice 资源使用 (Top 5):${RESET}"
for slice in $(systemctl list-units --type=slice --state=active --no-legend | awk '{print $1}' | head -5); do
MEM_CURRENT=$(systemctl show "$slice" --property=MemoryCurrent --value 2>/dev/null)
MEM_MAX=$(systemctl show "$slice" --property=MemoryMax --value 2>/dev/null)
CPU_WEIGHT=$(systemctl show "$slice" --property=CPUWeight --value 2>/dev/null)
MEM_DISP=""
if [ -n "$MEM_CURRENT" ] && [ "$MEM_CURRENT" != "[not set]" ]; then
MEM_DISP="内存: $(numfmt --to=iec $MEM_CURRENT 2>/dev/null || echo $MEM_CURRENT)"
fi
if [ -n "$CPU_WEIGHT" ] && [ "$CPU_WEIGHT" != "[not set]" ]; then
MEM_DISP="$MEM_DISP CPU权重: $CPU_WEIGHT"
fi
if [ -n "$MEM_DISP" ]; then
printf " ${BRIGHT_MAGENTA}📦${RESET} ${BRIGHT_WHITE}%-25s${RESET} ${LIME}%s${RESET}\n" "$slice" "$MEM_DISP"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块19: 系统性能信息
# ═══════════════════════════════════════════════════════════════════════════════
module_performance() {
print_section "📊 系统性能信息"
# 获取 CPU 使用率
CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)
# 获取内存信息
MEM_INFO=$(free -h | grep "Mem:")
MEM_TOTAL=$(echo "$MEM_INFO" | awk '{print $2}')
MEM_USED=$(echo "$MEM_INFO" | awk '{print $3}')
MEM_FREE=$(echo "$MEM_INFO" | awk '{print $4}')
MEM_PERCENT=$(free | grep "Mem:" | awk '{printf "%.1f", $3/$2*100}')
# 获取启动时间
BOOT_TIME_SEC=$(systemctl show --property=UserspaceTimestampMonotonic --value | cut -d' ' -f1)
BOOT_TIME_SEC=${BOOT_TIME_SEC:-0}
BOOT_TIME_SEC=$((BOOT_TIME_SEC / 1000000))
print_subitem "CPU 使用率" "${CPU_USAGE}%" "${BRIGHT_YELLOW}"
print_subitem "内存总量" "$MEM_TOTAL" "${BRIGHT_CYAN}"
print_subitem "已用内存" "$MEM_USED (${MEM_PERCENT}%)" "${ORANGE}"
print_subitem "可用内存" "$MEM_FREE" "${LIME}"
print_subitem "启动耗时" "${BOOT_TIME_SEC}" "${PURPLE}"
# Swap 信息
SWAP_TOTAL=$(free -h | grep "Swap:" | awk '{print $2}')
SWAP_USED=$(free -h | grep "Swap:" | awk '{print $3}')
SWAP_FREE=$(free -h | grep "Swap:" | awk '{print $4}')
if [ "$SWAP_TOTAL" != "0" ]; then
print_subitem "Swap总量" "$SWAP_TOTAL" "${PINK}"
print_subitem "Swap已用" "$SWAP_USED" "${PINK}"
print_subitem "Swap可用" "$SWAP_FREE" "${PINK}"
fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块20: 电源管理状态
# ═══════════════════════════════════════════════════════════════════════════════
module_power_management() {
print_section "🔋 电源管理状态"
# 检查 systemd-suspend 服务
if systemctl list-unit-files | grep -q "systemd-suspend.service"; then
SUSPEND_STATE=$(systemctl is-enabled systemd-suspend.service 2>/dev/null || echo "N/A")
print_subitem "Suspend 服务" "$SUSPEND_STATE" "${BRIGHT_CYAN}"
fi
# 检查 systemd-hibernate 服务
if systemctl list-unit-files | grep -q "systemd-hibernate.service"; then
HIBERNATE_STATE=$(systemctl is-enabled systemd-hibernate.service 2>/dev/null || echo "N/A")
print_subitem "Hibernate 服务" "$HIBERNATE_STATE" "${BRIGHT_CYAN}"
fi
# 检查 logind 状态
if systemctl list-unit-files | grep -q "systemd-logind.service"; then
LOGIND_STATE=$(systemctl is-active systemd-logind.service 2>/dev/null || echo "N/A")
print_subitem "Logind 状态" "$LOGIND_STATE" "${LIME}"
fi
# 显示电源相关事件
echo -e "${BRIGHT_CYAN}最近的电源相关日志:${RESET}"
journalctl -u systemd-logind -u upower -n 3 --no-pager 2>/dev/null | while IFS= read -r line; do
if [ -n "$line" ]; then
echo -e " ${CYAN}${RESET} ${WHITE}${line}${RESET}"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块21: 关键服务状态
# ═══════════════════════════════════════════════════════════════════════════════
module_critical_services() {
print_section "🔑 关键系统服务状态"
KEY_SERVICES=(
"sshd.service"
"NetworkManager.service"
"cron.service"
"rsyslog.service"
"dbus.service"
"systemd-logind.service"
"systemd-journald.service"
"systemd-udevd.service"
"polkit.service"
)
for service in "${KEY_SERVICES[@]}"; do
if systemctl list-unit-files 2>/dev/null | grep -q "$service"; then
SERVICE_STATE=$(systemctl is-active "$service" 2>/dev/null)
SERVICE_ENABLED=$(systemctl is-enabled "$service" 2>/dev/null)
case $SERVICE_STATE in
active) STATE_ICON="✅"; STATE_COLOR="${BRIGHT_GREEN}" ;;
inactive) STATE_ICON="⭕"; STATE_COLOR="${BRIGHT_YELLOW}" ;;
failed) STATE_ICON="❌"; STATE_COLOR="${BRIGHT_RED}" ;;
*) STATE_ICON="❓"; STATE_COLOR="${BRIGHT_WHITE}" ;;
esac
printf " ${STATE_ICON} ${BRIGHT_WHITE}%-30s${RESET} ${STATE_COLOR}%-10s${RESET} ${CYAN}启用:%s${RESET}\n" "$service" "$SERVICE_STATE" "$SERVICE_ENABLED"
fi
done
}
# ═══════════════════════════════════════════════════════════════════════════════
# 模块22: 常用命令提示
# ═══════════════════════════════════════════════════════════════════════════════
module_help() {
print_section "💡 常用 Systemctl 命令"
echo -e "${BRIGHT_YELLOW}=== 服务管理 ===${RESET}"
echo -e "${BRIGHT_WHITE}systemctl status <service>${RESET} - 查看服务状态"
echo -e "${BRIGHT_WHITE}systemctl start <service>${RESET} - 启动服务"
echo -e "${BRIGHT_WHITE}systemctl stop <service>${RESET} - 停止服务"
echo -e "${BRIGHT_WHITE}systemctl restart <service>${RESET} - 重启服务"
echo -e "${BRIGHT_WHITE}systemctl enable <service>${RESET} - 启用开机自启"
echo -e "${BRIGHT_WHITE}systemctl disable <service>${RESET} - 禁用开机自启"
echo -e "${BRIGHT_WHITE}systemctl mask <service>${RESET} - 屏蔽服务"
echo -e "${BRIGHT_WHITE}systemctl unmask <service>${RESET} - 取消屏蔽"
echo -e "${BRIGHT_YELLOW}=== 状态查看 ===${RESET}"
echo -e "${BRIGHT_WHITE}systemctl is-active <service>${RESET} - 检查服务是否活跃"
echo -e "${BRIGHT_WHITE}systemctl is-enabled <service>${RESET} - 检查服务是否启用"
echo -e "${BRIGHT_WHITE}systemctl --failed${RESET} - 查看失败的服务"
echo -e "${BRIGHT_WHITE}systemctl list-dependencies <unit>${RESET} - 查看依赖"
echo -e "${BRIGHT_YELLOW}=== 日志查看 ===${RESET}"
echo -e "${BRIGHT_WHITE}journalctl -u <service>${RESET} - 查看服务日志"
echo -e "${BRIGHT_WHITE}journalctl -xe${RESET} - 查看最近日志"
echo -e "${BRIGHT_WHITE}journalctl -p err${RESET} - 查看错误日志"
echo -e "${BRIGHT_YELLOW}=== 电源管理 ===${RESET}"
echo -e "${BRIGHT_WHITE}systemctl suspend${RESET} - 挂起"
echo -e "${BRIGHT_WHITE}systemctl hibernate${RESET} - 休眠"
echo -e "${BRIGHT_WHITE}systemctl reboot${RESET} - 重启"
echo -e "${BRIGHT_WHITE}systemctl poweroff${RESET} - 关机"
}
# ═══════════════════════════════════════════════════════════════════════════════
# 主函数 - 模块调度
# ═══════════════════════════════════════════════════════════════════════════════
main() {
print_header
# 基础信息模块
module_systemd_version
module_system_info
module_systemd_status
# 单元统计模块
module_service_stats
module_running_services
module_failed_services
module_masked_services
# 各类单元模块
module_timer
module_socket
module_target
module_mount
module_path
module_device
module_scope_slice
# 系统信息模块
module_dependencies
module_journal
module_environment
module_cgroup
module_performance
module_power_management
# 服务状态模块
module_critical_services
# 帮助信息
module_help
# 结束
echo -e "${DASH_SEPARATOR}"
echo -e "${BRIGHT_MAGENTA}✨ 信息收集完成!时间: $(date '+%Y-%m-%d %H:%M:%S')${RESET}"
echo -e "${DASH_SEPARATOR}"
}
# 执行主函数
main "@"

View File

@@ -0,0 +1,23 @@
version: "3.9"
services:
backend:
build:
context: .
dockerfile: Dockerfile
container_name: mengyaconnect-backend
environment:
# 后端监听端口
- PORT=8080
# 数据目录(容器内),会挂载到宿主机目录
- DATA_DIR=/app/data
# 可按需放开 CORS / WebSocket 来源
- ALLOWED_ORIGINS=*
ports:
# 宿主机 2431 → 容器 8080
- "2431:8080"
volumes:
# 持久化数据目录
- /shumengya/docker/mengyaconnect-backend/data/:/app/data
restart: unless-stopped

View File

@@ -0,0 +1,37 @@
module mengyaconnect-backend
go 1.21
require (
github.com/gin-gonic/gin v1.10.0
github.com/gorilla/websocket v1.5.1
golang.org/x/crypto v0.23.0
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -0,0 +1,93 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -0,0 +1,715 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
// ─── 持久化数据类型 ───────────────────────────────────────────────
type SSHProfile struct {
Name string `json:"name,omitempty"` // 文件名(不含 .json
Alias string `json:"alias"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
PrivateKey string `json:"privateKey,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
}
type Command struct {
Alias string `json:"alias"`
Command string `json:"command"`
}
type ScriptInfo struct {
Name string `json:"name"`
Content string `json:"content,omitempty"`
}
// 配置与数据目录辅助函数见 config.go
type wsMessage struct {
Type string `json:"type"`
Host string `json:"host,omitempty"`
Port int `json:"port,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
PrivateKey string `json:"privateKey,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Data string `json:"data,omitempty"`
Cols int `json:"cols,omitempty"`
Rows int `json:"rows,omitempty"`
Status string `json:"status,omitempty"`
Message string `json:"message,omitempty"`
}
type wsWriter struct {
conn *websocket.Conn
mu sync.Mutex
}
func (w *wsWriter) send(msg wsMessage) {
w.mu.Lock()
defer w.mu.Unlock()
_ = w.conn.WriteJSON(msg)
}
func main() {
if mode := os.Getenv("GIN_MODE"); mode != "" {
gin.SetMode(mode)
}
router := gin.New()
router.Use(gin.Logger(), gin.Recovery(), corsMiddleware())
allowedOrigins := parseListEnv("ALLOWED_ORIGINS")
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return isOriginAllowed(r.Header.Get("Origin"), allowedOrigins)
},
}
// ─── 基本配置 CRUD ──────────────────────────────────────────
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
})
router.GET("/api/ws/ssh", func(c *gin.Context) {
handleSSHWebSocket(c, upgrader)
})
// ─── SSH 配置 CRUD ──────────────────────────────────────────
router.GET("/api/ssh", handleListSSH)
router.POST("/api/ssh", handleCreateSSH)
router.PUT("/api/ssh/:name", handleUpdateSSH)
router.DELETE("/api/ssh/:name", handleDeleteSSH)
// ─── 快捷命令 CRUD ─────────────────────────────────────────
router.GET("/api/commands", handleListCommands)
router.POST("/api/commands", handleCreateCommand)
router.PUT("/api/commands/:index", handleUpdateCommand)
router.DELETE("/api/commands/:index", handleDeleteCommand)
// ─── 脚本 CRUD ─────────────────────────────────────────────
router.GET("/api/scripts", handleListScripts)
router.GET("/api/scripts/:name", handleGetScript)
router.POST("/api/scripts", handleCreateScript)
router.PUT("/api/scripts/:name", handleUpdateScript)
router.DELETE("/api/scripts/:name", handleDeleteScript)
addr := getEnv("ADDR", ":"+getEnv("PORT", "8080"))
server := &http.Server{
Addr: addr,
Handler: router,
ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("SSH WebSocket server listening on %s", addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server error: %v", err)
}
}
func handleSSHWebSocket(c *gin.Context, upgrader websocket.Upgrader) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
defer conn.Close()
conn.SetReadLimit(1 << 20)
writer := &wsWriter{conn: conn}
writer.send(wsMessage{Type: "status", Status: "connected", Message: "WebSocket connected"})
var (
sshClient *ssh.Client
sshSession *ssh.Session
sshStdin io.WriteCloser
stdout io.Reader
stderr io.Reader
cancelFn context.CancelFunc
)
cleanup := func() {
if cancelFn != nil {
cancelFn()
}
if sshSession != nil {
_ = sshSession.Close()
}
if sshClient != nil {
_ = sshClient.Close()
}
}
defer cleanup()
for {
var msg wsMessage
if err := conn.ReadJSON(&msg); err != nil {
writer.send(wsMessage{Type: "status", Status: "closed", Message: "WebSocket closed"})
return
}
switch msg.Type {
case "connect":
if sshSession != nil {
writer.send(wsMessage{Type: "error", Message: "SSH session already exists"})
continue
}
client, session, stdin, out, errOut, err := startSSHSession(msg)
if err != nil {
writer.send(wsMessage{Type: "error", Message: err.Error()})
continue
}
sshClient = client
sshSession = session
sshStdin = stdin
stdout = out
stderr = errOut
ctx, cancel := context.WithCancel(context.Background())
cancelFn = cancel
go streamToWebSocket(ctx, writer, stdout)
go streamToWebSocket(ctx, writer, stderr)
go func() {
_ = session.Wait()
writer.send(wsMessage{Type: "status", Status: "closed", Message: "SSH session closed"})
cleanup()
}()
writer.send(wsMessage{Type: "status", Status: "ready", Message: "SSH connected"})
case "input":
if sshStdin == nil {
writer.send(wsMessage{Type: "error", Message: "SSH session not ready"})
continue
}
if msg.Data != "" {
_, _ = sshStdin.Write([]byte(msg.Data))
}
case "resize":
if sshSession == nil {
continue
}
rows := msg.Rows
cols := msg.Cols
if rows > 0 && cols > 0 {
_ = sshSession.WindowChange(rows, cols)
}
case "ping":
writer.send(wsMessage{Type: "pong"})
case "close":
writer.send(wsMessage{Type: "status", Status: "closing", Message: "Closing SSH session"})
return
}
}
}
func startSSHSession(msg wsMessage) (*ssh.Client, *ssh.Session, io.WriteCloser, io.Reader, io.Reader, error) {
host := strings.TrimSpace(msg.Host)
if host == "" {
return nil, nil, nil, nil, nil, errors.New("host is required")
}
port := msg.Port
if port == 0 {
port = 22
}
user := strings.TrimSpace(msg.Username)
if user == "" {
return nil, nil, nil, nil, nil, errors.New("username is required")
}
auths, err := buildAuthMethods(msg)
if err != nil {
return nil, nil, nil, nil, nil, err
}
cfg := &ssh.ClientConfig{
User: user,
Auth: auths,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 12 * time.Second,
}
addr := fmt.Sprintf("%s:%d", host, port)
client, err := ssh.Dial("tcp", addr, cfg)
if err != nil {
return nil, nil, nil, nil, nil, fmt.Errorf("ssh dial failed: %w", err)
}
session, err := client.NewSession()
if err != nil {
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("ssh session failed: %w", err)
}
rows := msg.Rows
cols := msg.Cols
if rows == 0 {
rows = 24
}
if cols == 0 {
cols = 80
}
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil {
_ = session.Close()
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("request pty failed: %w", err)
}
stdin, err := session.StdinPipe()
if err != nil {
_ = session.Close()
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("stdin pipe failed: %w", err)
}
stdout, err := session.StdoutPipe()
if err != nil {
_ = session.Close()
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("stdout pipe failed: %w", err)
}
stderr, err := session.StderrPipe()
if err != nil {
_ = session.Close()
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("stderr pipe failed: %w", err)
}
if err := session.Shell(); err != nil {
_ = session.Close()
_ = client.Close()
return nil, nil, nil, nil, nil, fmt.Errorf("shell start failed: %w", err)
}
return client, session, stdin, stdout, stderr, nil
}
func buildAuthMethods(msg wsMessage) ([]ssh.AuthMethod, error) {
var methods []ssh.AuthMethod
if strings.TrimSpace(msg.PrivateKey) != "" {
signer, err := parsePrivateKey(msg.PrivateKey, msg.Passphrase)
if err != nil {
return nil, fmt.Errorf("private key error: %w", err)
}
methods = append(methods, ssh.PublicKeys(signer))
}
if msg.Password != "" {
methods = append(methods, ssh.Password(msg.Password))
}
if len(methods) == 0 {
return nil, errors.New("no auth method provided")
}
return methods, nil
}
func parsePrivateKey(key, passphrase string) (ssh.Signer, error) {
key = strings.TrimSpace(key)
if passphrase != "" {
return ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(passphrase))
}
return ssh.ParsePrivateKey([]byte(key))
}
func streamToWebSocket(ctx context.Context, writer *wsWriter, reader io.Reader) {
buf := make([]byte, 8192)
for {
select {
case <-ctx.Done():
return
default:
}
n, err := reader.Read(buf)
if n > 0 {
writer.send(wsMessage{Type: "output", Data: string(buf[:n])})
}
if err != nil {
return
}
}
}
// CORS、中间件与环境变量工具函数见 config.go
// ═══════════════════════════════════════════════════════════════════
// SSH 配置 CRUD
// ═══════════════════════════════════════════════════════════════════
// GET /api/ssh — 列出所有 SSH 配置
func handleListSSH(c *gin.Context) {
entries, err := os.ReadDir(sshDir())
if err != nil {
c.JSON(http.StatusOK, gin.H{"data": []SSHProfile{}})
return
}
var profiles []SSHProfile
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
raw, err := os.ReadFile(filepath.Join(sshDir(), e.Name()))
if err != nil {
continue
}
var p SSHProfile
if err := json.Unmarshal(raw, &p); err != nil {
continue
}
p.Name = strings.TrimSuffix(e.Name(), ".json")
profiles = append(profiles, p)
}
if profiles == nil {
profiles = []SSHProfile{}
}
c.JSON(http.StatusOK, gin.H{"data": profiles})
}
// POST /api/ssh — 新建 SSH 配置
func handleCreateSSH(c *gin.Context) {
var p SSHProfile
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if p.Alias == "" || p.Host == "" || p.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
return
}
name := p.Name
if name == "" {
name = p.Alias
}
safe, err := sanitizeName(strings.ReplaceAll(name, " ", "-"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
p.Name = ""
raw, _ := json.MarshalIndent(p, "", " ")
if err := os.MkdirAll(sshDir(), 0o750); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dir"})
return
}
if err := os.WriteFile(filepath.Join(sshDir(), safe+".json"), raw, 0o600); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write file"})
return
}
p.Name = safe
c.JSON(http.StatusOK, gin.H{"data": p})
}
// PUT /api/ssh/:name — 更新 SSH 配置
func handleUpdateSSH(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
var p SSHProfile
if err := c.ShouldBindJSON(&p); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if p.Alias == "" || p.Host == "" || p.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
return
}
p.Name = ""
raw, _ := json.MarshalIndent(p, "", " ")
filePath := filepath.Join(sshDir(), name+".json")
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if err := os.WriteFile(filePath, raw, 0o600); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write file"})
return
}
p.Name = name
c.JSON(http.StatusOK, gin.H{"data": p})
}
// DELETE /api/ssh/:name — 删除 SSH 配置
func handleDeleteSSH(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
if err := os.Remove(filepath.Join(sshDir(), name+".json")); err != nil {
if os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete"})
}
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ═══════════════════════════════════════════════════════════════════
// 快捷命令 CRUD
// ═══════════════════════════════════════════════════════════════════
func readCommands() ([]Command, error) {
raw, err := os.ReadFile(cmdFilePath())
if err != nil {
if os.IsNotExist(err) {
return []Command{}, nil
}
return nil, err
}
var cmds []Command
if err := json.Unmarshal(raw, &cmds); err != nil {
return nil, err
}
return cmds, nil
}
func writeCommands(cmds []Command) error {
raw, err := json.MarshalIndent(cmds, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(cmdFilePath()), 0o750); err != nil {
return err
}
return os.WriteFile(cmdFilePath(), raw, 0o600)
}
// GET /api/commands
func handleListCommands(c *gin.Context) {
cmds, err := readCommands()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
return
}
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
// POST /api/commands
func handleCreateCommand(c *gin.Context) {
var cmd Command
if err := c.ShouldBindJSON(&cmd); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if cmd.Alias == "" || cmd.Command == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
return
}
cmds, err := readCommands()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
return
}
cmds = append(cmds, cmd)
if err := writeCommands(cmds); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
return
}
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
// PUT /api/commands/:index
func handleUpdateCommand(c *gin.Context) {
idx, err := strconv.Atoi(c.Param("index"))
if err != nil || idx < 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
return
}
var cmd Command
if err := c.ShouldBindJSON(&cmd); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if cmd.Alias == "" || cmd.Command == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
return
}
cmds, err := readCommands()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
return
}
if idx >= len(cmds) {
c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
return
}
cmds[idx] = cmd
if err := writeCommands(cmds); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
return
}
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
// DELETE /api/commands/:index
func handleDeleteCommand(c *gin.Context) {
idx, err := strconv.Atoi(c.Param("index"))
if err != nil || idx < 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
return
}
cmds, err := readCommands()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
return
}
if idx >= len(cmds) {
c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
return
}
cmds = append(cmds[:idx], cmds[idx+1:]...)
if err := writeCommands(cmds); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
return
}
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
// ═══════════════════════════════════════════════════════════════════
// 脚本 CRUD
// ═══════════════════════════════════════════════════════════════════
// GET /api/scripts — 列出所有脚本名称
func handleListScripts(c *gin.Context) {
entries, err := os.ReadDir(scriptDir())
if err != nil {
c.JSON(http.StatusOK, gin.H{"data": []ScriptInfo{}})
return
}
var scripts []ScriptInfo
for _, e := range entries {
if !e.IsDir() {
scripts = append(scripts, ScriptInfo{Name: e.Name()})
}
}
if scripts == nil {
scripts = []ScriptInfo{}
}
c.JSON(http.StatusOK, gin.H{"data": scripts})
}
// GET /api/scripts/:name — 获取脚本内容
func handleGetScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
raw, err := os.ReadFile(filepath.Join(scriptDir(), name))
if err != nil {
if os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read"})
}
return
}
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: string(raw)}})
}
// POST /api/scripts — 新建脚本
func handleCreateScript(c *gin.Context) {
var s ScriptInfo
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if s.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "name 为必填项"})
return
}
name, err := sanitizeName(s.Name)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
if err := os.MkdirAll(scriptDir(), 0o750); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dir"})
return
}
if err := os.WriteFile(filepath.Join(scriptDir(), name), []byte(s.Content), 0o640); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write"})
return
}
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
}
// PUT /api/scripts/:name — 更新脚本内容
func handleUpdateScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
filePath := filepath.Join(scriptDir(), name)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
var s ScriptInfo
if err := c.ShouldBindJSON(&s); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if err := os.WriteFile(filePath, []byte(s.Content), 0o640); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write"})
return
}
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
}
// DELETE /api/scripts/:name — 删除脚本
func handleDeleteScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
if err := os.Remove(filepath.Join(scriptDir(), name)); err != nil {
if os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete"})
}
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}

View File

@@ -0,0 +1,3 @@
VITE_API_BASE=https://ssh.api.shumengya.top/api
VITE_WS_URL=wss://ssh.api.shumengya.top/api/ws/ssh

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>萌芽SSH</title>
<meta name="description" content="柔和渐变风格的 Web SSH 连接面板,支持多窗口终端。" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1224
mengyaconnect-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "mengyaconnect-frontend",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.0.0",
"vue": "^3.4.38"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.2",
"vite": "^5.4.10"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
export function getApiBase() {
const envBase = import.meta.env.VITE_API_BASE;
if (envBase) {
return String(envBase).replace(/\/+$/, "");
}
// 默认走同源 /api更适合反向代理 + HTTPS
if (typeof window === "undefined") return "http://localhost:8080/api";
return `${window.location.origin}/api`;
}
export async function apiRequest(path, options = {}) {
const base = getApiBase();
const res = await fetch(`${base}${path}`, {
headers: {
"Content-Type": "application/json",
...(options.headers || {}),
},
...options,
});
let body = null;
try {
body = await res.json();
} catch {
body = null;
}
if (!res.ok) {
const message =
(body && body.error) || `请求失败 (${res.status} ${res.statusText})`;
throw new Error(message);
}
return body;
}

View File

@@ -0,0 +1,5 @@
import { createApp } from "vue";
import "@xterm/xterm/css/xterm.css";
import App from "./App.vue";
createApp(App).mount("#app");

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
server: {
host: true,
port: 5173,
},
});

0
萌芽连接 Normal file
View File