From f27996dde0c2b9644d43cf109e02d954db9cdf0a Mon Sep 17 00:00:00 2001 From: shumengya Date: Thu, 12 Mar 2026 15:01:48 +0800 Subject: [PATCH] shumengya mail@smyhub.com --- .gitignore | 30 + CLAUDE.md | 154 ++ mengyaconnect-backend/Dockerfile | 33 + mengyaconnect-backend/config.go | 78 + .../data/command/command.json | 22 + .../data/script/docker-info.sh | 253 +++ .../data/script/systemctl-info.sh | 802 +++++++ mengyaconnect-backend/docker-compose.yml | 23 + mengyaconnect-backend/go.mod | 37 + mengyaconnect-backend/go.sum | 93 + mengyaconnect-backend/main.go | 715 +++++++ mengyaconnect-frontend/.env.production | 3 + mengyaconnect-frontend/index.html | 13 + mengyaconnect-frontend/package-lock.json | 1224 +++++++++++ mengyaconnect-frontend/package.json | 20 + mengyaconnect-frontend/src/App.vue | 1902 +++++++++++++++++ mengyaconnect-frontend/src/api.js | 33 + mengyaconnect-frontend/src/main.js | 5 + mengyaconnect-frontend/vite.config.js | 10 + 萌芽连接 | 0 20 files changed, 5450 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 mengyaconnect-backend/Dockerfile create mode 100644 mengyaconnect-backend/config.go create mode 100644 mengyaconnect-backend/data/command/command.json create mode 100644 mengyaconnect-backend/data/script/docker-info.sh create mode 100644 mengyaconnect-backend/data/script/systemctl-info.sh create mode 100644 mengyaconnect-backend/docker-compose.yml create mode 100644 mengyaconnect-backend/go.mod create mode 100644 mengyaconnect-backend/go.sum create mode 100644 mengyaconnect-backend/main.go create mode 100644 mengyaconnect-frontend/.env.production create mode 100644 mengyaconnect-frontend/index.html create mode 100644 mengyaconnect-frontend/package-lock.json create mode 100644 mengyaconnect-frontend/package.json create mode 100644 mengyaconnect-frontend/src/App.vue create mode 100644 mengyaconnect-frontend/src/api.js create mode 100644 mengyaconnect-frontend/src/main.js create mode 100644 mengyaconnect-frontend/vite.config.js create mode 100644 萌芽连接 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e2438 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bccb4e5 --- /dev/null +++ b/CLAUDE.md @@ -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/` + +`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 `:`) +- `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, you’ll usually run: +- backend on `localhost:8080` +- frontend on `localhost:5173` diff --git a/mengyaconnect-backend/Dockerfile b/mengyaconnect-backend/Dockerfile new file mode 100644 index 0000000..df15b00 --- /dev/null +++ b/mengyaconnect-backend/Dockerfile @@ -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"] + diff --git a/mengyaconnect-backend/config.go b/mengyaconnect-backend/config.go new file mode 100644 index 0000000..0ffdd13 --- /dev/null +++ b/mengyaconnect-backend/config.go @@ -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 +} + diff --git a/mengyaconnect-backend/data/command/command.json b/mengyaconnect-backend/data/command/command.json new file mode 100644 index 0000000..64dcf21 --- /dev/null +++ b/mengyaconnect-backend/data/command/command.json @@ -0,0 +1,22 @@ +[ + { + "alias": "安全关机", + "command": "shutdown -h now" + }, + { + "alias": "定时十分钟后关机", + "command": "shutdown -h 10" + }, + { + "alias": "重启", + "command": "reboot" + }, + { + "alias": "输出当前目录", + "command": "pwd" + }, + { + "alias": "列出当前目录文件", + "command": "ls -al" + } +] \ No newline at end of file diff --git a/mengyaconnect-backend/data/script/docker-info.sh b/mengyaconnect-backend/data/script/docker-info.sh new file mode 100644 index 0000000..83e2034 --- /dev/null +++ b/mengyaconnect-backend/data/script/docker-info.sh @@ -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 "" | 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 "$@" diff --git a/mengyaconnect-backend/data/script/systemctl-info.sh b/mengyaconnect-backend/data/script/systemctl-info.sh new file mode 100644 index 0000000..b50dda0 --- /dev/null +++ b/mengyaconnect-backend/data/script/systemctl-info.sh @@ -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 ${RESET} - 查看服务状态" + echo -e "${BRIGHT_WHITE}systemctl start ${RESET} - 启动服务" + echo -e "${BRIGHT_WHITE}systemctl stop ${RESET} - 停止服务" + echo -e "${BRIGHT_WHITE}systemctl restart ${RESET} - 重启服务" + echo -e "${BRIGHT_WHITE}systemctl enable ${RESET} - 启用开机自启" + echo -e "${BRIGHT_WHITE}systemctl disable ${RESET} - 禁用开机自启" + echo -e "${BRIGHT_WHITE}systemctl mask ${RESET} - 屏蔽服务" + echo -e "${BRIGHT_WHITE}systemctl unmask ${RESET} - 取消屏蔽" + + echo -e "${BRIGHT_YELLOW}=== 状态查看 ===${RESET}" + echo -e "${BRIGHT_WHITE}systemctl is-active ${RESET} - 检查服务是否活跃" + echo -e "${BRIGHT_WHITE}systemctl is-enabled ${RESET} - 检查服务是否启用" + echo -e "${BRIGHT_WHITE}systemctl --failed${RESET} - 查看失败的服务" + echo -e "${BRIGHT_WHITE}systemctl list-dependencies ${RESET} - 查看依赖" + + echo -e "${BRIGHT_YELLOW}=== 日志查看 ===${RESET}" + echo -e "${BRIGHT_WHITE}journalctl -u ${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 "@" \ No newline at end of file diff --git a/mengyaconnect-backend/docker-compose.yml b/mengyaconnect-backend/docker-compose.yml new file mode 100644 index 0000000..aa3ab1b --- /dev/null +++ b/mengyaconnect-backend/docker-compose.yml @@ -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 + diff --git a/mengyaconnect-backend/go.mod b/mengyaconnect-backend/go.mod new file mode 100644 index 0000000..d3099da --- /dev/null +++ b/mengyaconnect-backend/go.mod @@ -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 +) diff --git a/mengyaconnect-backend/go.sum b/mengyaconnect-backend/go.sum new file mode 100644 index 0000000..5931dcc --- /dev/null +++ b/mengyaconnect-backend/go.sum @@ -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= diff --git a/mengyaconnect-backend/main.go b/mengyaconnect-backend/main.go new file mode 100644 index 0000000..2366a63 --- /dev/null +++ b/mengyaconnect-backend/main.go @@ -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"}) +} diff --git a/mengyaconnect-frontend/.env.production b/mengyaconnect-frontend/.env.production new file mode 100644 index 0000000..ffa7b2a --- /dev/null +++ b/mengyaconnect-frontend/.env.production @@ -0,0 +1,3 @@ +VITE_API_BASE=https://ssh.api.shumengya.top/api +VITE_WS_URL=wss://ssh.api.shumengya.top/api/ws/ssh + diff --git a/mengyaconnect-frontend/index.html b/mengyaconnect-frontend/index.html new file mode 100644 index 0000000..a28c0c1 --- /dev/null +++ b/mengyaconnect-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + 萌芽SSH + + + +
+ + + diff --git a/mengyaconnect-frontend/package-lock.json b/mengyaconnect-frontend/package-lock.json new file mode 100644 index 0000000..c47143c --- /dev/null +++ b/mengyaconnect-frontend/package-lock.json @@ -0,0 +1,1224 @@ +{ + "name": "mengyaconnect-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mengyaconnect-frontend", + "version": "0.1.0", + "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" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/mengyaconnect-frontend/package.json b/mengyaconnect-frontend/package.json new file mode 100644 index 0000000..1da1fea --- /dev/null +++ b/mengyaconnect-frontend/package.json @@ -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" + } +} diff --git a/mengyaconnect-frontend/src/App.vue b/mengyaconnect-frontend/src/App.vue new file mode 100644 index 0000000..9cc01c7 --- /dev/null +++ b/mengyaconnect-frontend/src/App.vue @@ -0,0 +1,1902 @@ + + + + + + + diff --git a/mengyaconnect-frontend/src/api.js b/mengyaconnect-frontend/src/api.js new file mode 100644 index 0000000..7695375 --- /dev/null +++ b/mengyaconnect-frontend/src/api.js @@ -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; +} + diff --git a/mengyaconnect-frontend/src/main.js b/mengyaconnect-frontend/src/main.js new file mode 100644 index 0000000..0f6ac67 --- /dev/null +++ b/mengyaconnect-frontend/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from "vue"; +import "@xterm/xterm/css/xterm.css"; +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/mengyaconnect-frontend/vite.config.js b/mengyaconnect-frontend/vite.config.js new file mode 100644 index 0000000..d828d96 --- /dev/null +++ b/mengyaconnect-frontend/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], + server: { + host: true, + port: 5173, + }, +}); diff --git a/萌芽连接 b/萌芽连接 new file mode 100644 index 0000000..e69de29