chore: sync local updates

This commit is contained in:
2026-05-16 19:03:04 +08:00
parent b86ce4a73a
commit 14cd94c88d
45 changed files with 2983 additions and 4127 deletions

View File

@@ -0,0 +1,5 @@
data/
docker-compose.yml
.git
.gitignore
dist/

View File

@@ -1,33 +1,23 @@
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"]
# 与 go.mod 中 go 版本一致modernc.org/sqlite 等依赖会抬高 toolchain 要求)
FROM golang:1.25-alpine AS builder
WORKDIR /app
RUN apk add --no-cache ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
ARG TARGETOS=linux
ARG TARGETARCH
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} 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 /mengyaconnect-backend /app/mengyaconnect-backend
ENV DATA_DIR=/app/data
ENV PORT=8080
RUN chmod +x /app/mengyaconnect-backend && \
mkdir -p "$DATA_DIR" && chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["/app/mengyaconnect-backend"]

View File

@@ -0,0 +1,5 @@
@echo off
setlocal
cd /d "%~dp0"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-linux.ps1"
exit /b %ERRORLEVEL%

View File

@@ -0,0 +1,12 @@
$ErrorActionPreference = "Stop"
Set-Location $PSScriptRoot
New-Item -ItemType Directory -Force -Path dist | Out-Null
$env:CGO_ENABLED = "0"
$env:GOOS = "linux"
$env:GOARCH = "amd64"
$out = Join-Path $PSScriptRoot "dist\mengyaconnect-backend"
go build -trimpath -ldflags="-s -w" -o $out .
$b = [IO.File]::ReadAllBytes($out)
if ($b.Length -ge 4 -and $b[0] -eq 0x7F -and $b[1] -eq 0x45) { Write-Host "OK: Linux ELF amd64 -> $out" }
elseif ($b[0] -eq 0x4D -and $b[1] -eq 0x5A) { throw "Got Windows PE — check go env" }
else { throw "Unknown output format" }

View File

@@ -1,78 +1,80 @@
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
}
package main
import (
"errors"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
func dataBasePath() string { return getEnv("DATA_DIR", "data") }
func dbPath() string { return filepath.Join(dataBasePath(), "mengyaconnect.db") }
// accessPassword 返回 Web 访问密码,可通过 ACCESS_PASSWORD 环境变量覆盖
func accessPassword() string { return getEnv("ACCESS_PASSWORD", "shumengya520") }
// apiVersion 根路径 JSON 中的版本号,可由 API_VERSION 覆盖
func apiVersion() string { return getEnv("API_VERSION", "1.0.0-go") }
// validateName 检查名称合法性(防止空/路径穿越等),不再依赖 filepath.Base
func validateName(name string) error {
name = strings.TrimSpace(name)
if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\\") {
return errors.New("invalid name")
}
return 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

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

View File

@@ -1,253 +0,0 @@
#!/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

@@ -1,802 +0,0 @@
#!/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 "@"

186
mengyaconnect-backend/db.go Normal file
View File

@@ -0,0 +1,186 @@
package main
import (
"database/sql"
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
_ "modernc.org/sqlite"
)
// DB is the global SQLite connection shared by all handlers.
var DB *sql.DB
const schema = `
CREATE TABLE IF NOT EXISTS ssh_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
alias TEXT NOT NULL,
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 22,
username TEXT NOT NULL,
password TEXT NOT NULL DEFAULT '',
private_key TEXT NOT NULL DEFAULT '',
passphrase TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT NOT NULL,
command TEXT NOT NULL,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS scripts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
content TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`
// initDB opens the SQLite database, applies PRAGMAs and creates tables.
func initDB() error {
if err := os.MkdirAll(dataBasePath(), 0o750); err != nil {
return err
}
db, err := sql.Open("sqlite", dbPath())
if err != nil {
return err
}
// Single writer; multiple readers are fine via WAL.
db.SetMaxOpenConns(1)
if _, err := db.Exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;`); err != nil {
return err
}
if _, err := db.Exec(schema); err != nil {
return err
}
DB = db
log.Printf("SQLite database ready: %s", dbPath())
return nil
}
// ─── One-time migration from legacy file storage ─────────────────────────────
// migrateFromFiles reads legacy JSON/text files and inserts their data into
// SQLite (INSERT OR IGNORE so duplicates are skipped). After migration each
// source file is renamed to *.migrated so the routine is not repeated.
func migrateFromFiles() {
migrateSSHProfiles()
migrateCommands()
migrateScripts()
}
func migrateSSHProfiles() {
dir := filepath.Join(dataBasePath(), "ssh")
entries, err := os.ReadDir(dir)
if err != nil {
return // directory doesn't exist — nothing to migrate
}
migrated := 0
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
path := filepath.Join(dir, e.Name())
raw, err := os.ReadFile(path)
if err != nil {
log.Printf("migrate ssh: read %s: %v", path, err)
continue
}
var p SSHProfile
if err := json.Unmarshal(raw, &p); err != nil {
log.Printf("migrate ssh: parse %s: %v", path, err)
continue
}
name := strings.TrimSuffix(e.Name(), ".json")
if p.Port == 0 {
p.Port = 22
}
_, err = DB.Exec(
`INSERT OR IGNORE INTO ssh_profiles (name,alias,host,port,username,password,private_key,passphrase)
VALUES (?,?,?,?,?,?,?,?)`,
name, p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase,
)
if err != nil {
log.Printf("migrate ssh: insert %s: %v", name, err)
continue
}
_ = os.Rename(path, path+".migrated")
migrated++
}
if migrated > 0 {
log.Printf("migrate: imported %d SSH profile(s) from %s", migrated, dir)
}
}
func migrateCommands() {
path := filepath.Join(dataBasePath(), "command", "command.json")
raw, err := os.ReadFile(path)
if err != nil {
return
}
var cmds []Command
if err := json.Unmarshal(raw, &cmds); err != nil {
log.Printf("migrate commands: parse %s: %v", path, err)
return
}
// Check if commands table is already populated to avoid duplicating on restart.
var count int
_ = DB.QueryRow(`SELECT COUNT(*) FROM commands`).Scan(&count)
if count > 0 {
log.Printf("migrate commands: table not empty, skipping file migration")
_ = os.Rename(path, path+".migrated")
return
}
for i, cmd := range cmds {
if _, err := DB.Exec(
`INSERT INTO commands (alias, command, sort_order) VALUES (?,?,?)`,
cmd.Alias, cmd.Command, i,
); err != nil {
log.Printf("migrate commands: insert [%d]: %v", i, err)
}
}
_ = os.Rename(path, path+".migrated")
log.Printf("migrate: imported %d command(s) from %s", len(cmds), path)
}
func migrateScripts() {
dir := filepath.Join(dataBasePath(), "script")
entries, err := os.ReadDir(dir)
if err != nil {
return
}
migrated := 0
for _, e := range entries {
if e.IsDir() {
continue
}
path := filepath.Join(dir, e.Name())
content, err := os.ReadFile(path)
if err != nil {
log.Printf("migrate scripts: read %s: %v", path, err)
continue
}
if _, err := DB.Exec(
`INSERT OR IGNORE INTO scripts (name, content) VALUES (?,?)`,
e.Name(), string(content),
); err != nil {
log.Printf("migrate scripts: insert %s: %v", e.Name(), err)
continue
}
_ = os.Rename(path, path+".migrated")
migrated++
}
if migrated > 0 {
log.Printf("migrate: imported %d script(s) from %s", migrated, dir)
}
}

View File

@@ -2,6 +2,7 @@ version: "3.9"
services:
backend:
# 镜像在构建阶段按宿主机架构编译;勿写死 platform: linux/amd64否则 ARM 云主机无 QEMU 时会 exec format error
build:
context: .
dockerfile: Dockerfile

View File

@@ -1,6 +1,6 @@
module mengyaconnect-backend
go 1.21
go 1.25.0
require (
github.com/gin-gonic/gin v1.10.0
@@ -13,25 +13,33 @@ require (
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/dustin/go-humanize v1.0.1 // 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/google/uuid v1.6.0 // 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/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/sys v0.42.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
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.50.0 // indirect
)

View File

@@ -9,6 +9,8 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
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=
@@ -28,6 +30,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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=
@@ -45,10 +49,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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=
@@ -76,6 +84,8 @@ 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/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
@@ -89,5 +99,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
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=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -2,14 +2,15 @@ package main
import (
"context"
"encoding/json"
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
@@ -23,12 +24,12 @@ import (
// ─── 持久化数据类型 ───────────────────────────────────────────────
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"`
Name string `json:"name,omitempty"`
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"`
}
@@ -43,7 +44,7 @@ type ScriptInfo struct {
Content string `json:"content,omitempty"`
}
// 配置与数据目录辅助函数见 config.go
// 配置与数据辅助函数见 config.go / db.go
type wsMessage struct {
Type string `json:"type"`
@@ -71,7 +72,84 @@ func (w *wsWriter) send(msg wsMessage) {
_ = w.conn.WriteJSON(msg)
}
// ─── 会话令牌(服务启动时随机生成一次)────────────────────────────────
var sessionToken string
func initSessionToken() {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
log.Fatalf("failed to generate session token: %v", err)
}
sessionToken = hex.EncodeToString(b)
}
// authMiddleware 校验请求中携带的令牌。
// WebSocket 不支持自定义 Header因此同时接受 ?token= 查询参数。
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := ""
if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
token = strings.TrimPrefix(auth, "Bearer ")
}
if token == "" {
token = c.Query("token")
}
if token != sessionToken {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
}
// POST /api/auth/login — 用密码换取令牌
func handleLogin(c *gin.Context) {
var body struct {
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if body.Password != accessPassword() {
c.JSON(http.StatusUnauthorized, gin.H{"error": "密码错误"})
return
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"token": sessionToken}})
}
// GET /api/auth/verify — 校验令牌是否有效(受 authMiddleware 保护)
func handleVerify(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": gin.H{"valid": true}})
}
// GET / — 服务说明(无需鉴权,便于网关 / 探活)
func handleAPIRoot(c *gin.Context) {
loc := time.FixedZone("CST", 8*3600)
c.JSON(http.StatusOK, gin.H{
"description": "提供 Web SSH 终端与跳板连接SSH 由服务端发起);浏览器访问需先 POST /api/auth/login 换取令牌,后续 API 与 WebSocket 携带 Bearer 或 ?token=",
"endpoints": gin.H{
"health": "/health",
"auth": "/api/auth/login (POST公开), /api/auth/verify (GET需令牌)",
"websocket_ssh": "/api/ws/ssh (GET 升级 WebSocket需令牌查询参数 token= 或 Header)",
"ssh_profiles": "/api/ssh, /api/ssh/:name (CRUD需令牌)",
"commands": "/api/commands, /api/commands/:index (CRUD需令牌)",
"scripts": "/api/scripts, /api/scripts/:name (CRUD需令牌)",
},
"message": "萌芽 SSH MengyaConnect 后端 API 服务运行中",
"timestamp": time.Now().In(loc).Format(time.RFC3339),
"version": apiVersion(),
})
}
func main() {
initSessionToken()
if err := initDB(); err != nil {
log.Fatalf("db init: %v", err)
}
migrateFromFiles()
if mode := os.Getenv("GIN_MODE"); mode != "" {
gin.SetMode(mode)
}
@@ -88,36 +166,43 @@ func main() {
},
}
// ─── 基本配置 CRUD ──────────────────────────────────────────
// ─── 不需要鉴权的路由 ────────────────────────────────────────
router.GET("/", handleAPIRoot)
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
})
router.POST("/api/auth/login", handleLogin)
router.GET("/api/ws/ssh", func(c *gin.Context) {
// ─── 需要鉴权的路由 ──────────────────────────────────────────
auth := router.Group("/api", authMiddleware())
auth.GET("/auth/verify", handleVerify)
auth.GET("/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)
// SSH 配置 CRUD
auth.GET("/ssh", handleListSSH)
auth.POST("/ssh", handleCreateSSH)
auth.PUT("/ssh/:name", handleUpdateSSH)
auth.DELETE("/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
auth.GET("/commands", handleListCommands)
auth.POST("/commands", handleCreateCommand)
auth.PUT("/commands/:index", handleUpdateCommand)
auth.DELETE("/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)
// 脚本 CRUD
auth.GET("/scripts", handleListScripts)
auth.GET("/scripts/:name", handleGetScript)
auth.POST("/scripts", handleCreateScript)
auth.PUT("/scripts/:name", handleUpdateScript)
auth.DELETE("/scripts/:name", handleDeleteScript)
addr := getEnv("ADDR", ":"+getEnv("PORT", "8080"))
server := &http.Server{
@@ -127,6 +212,7 @@ func main() {
}
log.Printf("SSH WebSocket server listening on %s", addr)
log.Printf("Access password configured (use ACCESS_PASSWORD env to override)")
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server error: %v", err)
}
@@ -365,41 +451,33 @@ func streamToWebSocket(ctx context.Context, writer *wsWriter, reader io.Reader)
}
}
// CORS、中间件与环境变量工具函数见 config.go
// ═══════════════════════════════════════════════════════════════════
// SSH 配置 CRUD
// ═══════════════════════════════════════════════════════════════════
// GET /api/ssh — 列出所有 SSH 配置
// GET /api/ssh
func handleListSSH(c *gin.Context) {
entries, err := os.ReadDir(sshDir())
rows, err := DB.Query(
`SELECT name,alias,host,port,username,password,private_key,passphrase
FROM ssh_profiles ORDER BY id`)
if err != nil {
c.JSON(http.StatusOK, gin.H{"data": []SSHProfile{}})
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
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
}
defer rows.Close()
profiles := []SSHProfile{}
for rows.Next() {
var p SSHProfile
if err := json.Unmarshal(raw, &p); err != nil {
if err := rows.Scan(&p.Name, &p.Alias, &p.Host, &p.Port,
&p.Username, &p.Password, &p.PrivateKey, &p.Passphrase); 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 配置
// POST /api/ssh
func handleCreateSSH(c *gin.Context) {
var p SSHProfile
if err := c.ShouldBindJSON(&p); err != nil {
@@ -410,33 +488,39 @@ func handleCreateSSH(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
return
}
name := p.Name
name := strings.TrimSpace(p.Name)
if name == "" {
name = p.Alias
}
safe, err := sanitizeName(strings.ReplaceAll(name, " ", "-"))
if err != nil {
name = strings.ReplaceAll(name, " ", "-")
if err := validateName(name); 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"})
if p.Port == 0 {
p.Port = 22
}
_, err := DB.Exec(
`INSERT INTO ssh_profiles (name,alias,host,port,username,password,private_key,passphrase)
VALUES (?,?,?,?,?,?,?,?)`,
name, p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase,
)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
}
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
p.Name = name
c.JSON(http.StatusOK, gin.H{"data": p})
}
// PUT /api/ssh/:name — 更新 SSH 配置
// PUT /api/ssh/:name
func handleUpdateSSH(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
name := c.Param("name")
if err := validateName(name); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
return
}
@@ -449,34 +533,42 @@ func handleUpdateSSH(c *gin.Context) {
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"})
if p.Port == 0 {
p.Port = 22
}
res, err := DB.Exec(
`UPDATE ssh_profiles
SET alias=?,host=?,port=?,username=?,password=?,private_key=?,passphrase=?,
updated_at=CURRENT_TIMESTAMP
WHERE name=?`,
p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase, name,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
return
}
if err := os.WriteFile(filePath, raw, 0o600); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write file"})
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
p.Name = name
c.JSON(http.StatusOK, gin.H{"data": p})
}
// DELETE /api/ssh/:name — 删除 SSH 配置
// DELETE /api/ssh/:name
func handleDeleteSSH(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
name := c.Param("name")
if err := validateName(name); 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"})
}
res, err := DB.Exec(`DELETE FROM ssh_profiles WHERE name=?`, name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
return
}
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
@@ -486,37 +578,49 @@ func handleDeleteSSH(c *gin.Context) {
// 快捷命令 CRUD
// ═══════════════════════════════════════════════════════════════════
func readCommands() ([]Command, error) {
raw, err := os.ReadFile(cmdFilePath())
func queryAllCommands() ([]Command, error) {
rows, err := DB.Query(`SELECT alias,command FROM commands ORDER BY sort_order, id`)
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
defer rows.Close()
cmds := []Command{}
for rows.Next() {
var cmd Command
if err := rows.Scan(&cmd.Alias, &cmd.Command); err != nil {
return nil, err
}
cmds = append(cmds, cmd)
}
return cmds, nil
}
func writeCommands(cmds []Command) error {
raw, err := json.MarshalIndent(cmds, "", " ")
// commandIDAtIndex returns the primary-key id of the command at 0-based position idx.
func commandIDAtIndex(idx int) (int64, error) {
rows, err := DB.Query(`SELECT id FROM commands ORDER BY sort_order, id`)
if err != nil {
return err
return 0, err
}
if err := os.MkdirAll(filepath.Dir(cmdFilePath()), 0o750); err != nil {
return err
defer rows.Close()
pos := 0
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return 0, err
}
if pos == idx {
return id, nil
}
pos++
}
return os.WriteFile(cmdFilePath(), raw, 0o600)
return 0, sql.ErrNoRows
}
// GET /api/commands
func handleListCommands(c *gin.Context) {
cmds, err := readCommands()
cmds, err := queryAllCommands()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
return
}
c.JSON(http.StatusOK, gin.H{"data": cmds})
@@ -533,16 +637,16 @@ func handleCreateCommand(c *gin.Context) {
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"})
var maxOrder int
_ = DB.QueryRow(`SELECT COALESCE(MAX(sort_order),0) FROM commands`).Scan(&maxOrder)
if _, err := DB.Exec(
`INSERT INTO commands (alias,command,sort_order) VALUES (?,?,?)`,
cmd.Alias, cmd.Command, maxOrder+1,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
return
}
cmds, _ := queryAllCommands()
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
@@ -562,20 +666,16 @@ func handleUpdateCommand(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
return
}
cmds, err := readCommands()
id, err := commandIDAtIndex(idx)
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"})
if _, err := DB.Exec(`UPDATE commands SET alias=?,command=? WHERE id=?`, cmd.Alias, cmd.Command, id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
return
}
cmds, _ := queryAllCommands()
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
@@ -586,20 +686,16 @@ func handleDeleteCommand(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
return
}
cmds, err := readCommands()
id, err := commandIDAtIndex(idx)
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"})
if _, err := DB.Exec(`DELETE FROM commands WHERE id=?`, id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
return
}
cmds, _ := queryAllCommands()
c.JSON(http.StatusOK, gin.H{"data": cmds})
}
@@ -607,45 +703,46 @@ func handleDeleteCommand(c *gin.Context) {
// 脚本 CRUD
// ═══════════════════════════════════════════════════════════════════
// GET /api/scripts — 列出所有脚本名称
// GET /api/scripts
func handleListScripts(c *gin.Context) {
entries, err := os.ReadDir(scriptDir())
rows, err := DB.Query(`SELECT name FROM scripts ORDER BY name`)
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()})
defer rows.Close()
scripts := []ScriptInfo{}
for rows.Next() {
var s ScriptInfo
if err := rows.Scan(&s.Name); err != nil {
continue
}
}
if scripts == nil {
scripts = []ScriptInfo{}
scripts = append(scripts, s)
}
c.JSON(http.StatusOK, gin.H{"data": scripts})
}
// GET /api/scripts/:name — 获取脚本内容
// GET /api/scripts/:name
func handleGetScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
name := c.Param("name")
if err := validateName(name); 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"})
}
var s ScriptInfo
err := DB.QueryRow(`SELECT name,content FROM scripts WHERE name=?`, name).
Scan(&s.Name, &s.Content)
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
} else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
return
}
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: string(raw)}})
c.JSON(http.StatusOK, gin.H{"data": s})
}
// POST /api/scripts — 新建脚本
// POST /api/scripts
func handleCreateScript(c *gin.Context) {
var s ScriptInfo
if err := c.ShouldBindJSON(&s); err != nil {
@@ -656,59 +753,63 @@ func handleCreateScript(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": "name 为必填项"})
return
}
name, err := sanitizeName(s.Name)
if err != nil {
if err := validateName(s.Name); 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"})
_, err := DB.Exec(`INSERT INTO scripts (name,content) VALUES (?,?)`, s.Name, s.Content)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE") {
c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
}
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}})
c.JSON(http.StatusOK, gin.H{"data": s})
}
// PUT /api/scripts/:name — 更新脚本内容
// PUT /api/scripts/:name
func handleUpdateScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
name := c.Param("name")
if err := validateName(name); 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"})
res, err := DB.Exec(
`UPDATE scripts SET content=?,updated_at=CURRENT_TIMESTAMP WHERE name=?`,
s.Content, name,
)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
return
}
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
}
// DELETE /api/scripts/:name — 删除脚本
// DELETE /api/scripts/:name
func handleDeleteScript(c *gin.Context) {
name, err := sanitizeName(c.Param("name"))
if err != nil {
name := c.Param("name")
if err := validateName(name); 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"})
}
res, err := DB.Exec(`DELETE FROM scripts WHERE name=?`, name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
return
}
if n, _ := res.RowsAffected(); n == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})