修复了一些错误 更新了AI.md(给ai看的)

This commit is contained in:
Falck
2026-05-02 19:21:50 +08:00
parent 0783428f80
commit 70c531860b
240 changed files with 5626 additions and 10790 deletions

View File

@@ -1,9 +1,3 @@
"""TUI 客户端 - 前后端分离的 TUI 前端
通过 HTTP 连接后端 nebula serve消费 JSON API
直接使用 ANSI 转义码绘制专业终端界面。
支持鼠标点击导航。
"""
import sys
import json
import time
@@ -18,7 +12,6 @@ import re
from typing import Optional
# ── ANSI 工具 ────────────────────────────────────────────
def fg(r, g, b): return f"\x1b[38;2;{r};{g};{b}m"
def bg(r, g, b): return f"\x1b[48;2;{r};{g};{b}m"
@@ -39,12 +32,10 @@ C = {
"bar_bg": (50, 50, 70),
}
# ── 鼠标转义 ────────────────────────────────────────────
_MOUSE_ON = "\x1b[?1000h\x1b[?1002h\x1b[?1006h"
_MOUSE_OFF = "\x1b[?1006l\x1b[?1002l\x1b[?1000l"
# ── HTTP 请求 ────────────────────────────────────────────
def http_get(url: str, timeout=5) -> Optional[str]:
try:
@@ -64,7 +55,6 @@ def backend_alive(host="127.0.0.1", port=8080) -> bool:
return False
# ── 布局工具 ────────────────────────────────────────────
def term_size():
return shutil.get_terminal_size((80, 24))
@@ -77,10 +67,8 @@ def hbar(width: int, percent: float, color_fg=(0, 255, 135), color_bg=(50, 50, 7
return bar
# ── TUI 客户端 ──────────────────────────────────────────
Page = dict # {"id": str, "label": str, "desc": str}
Page = dict
class TUIClient:
_resize_flag = False
@@ -108,7 +96,6 @@ class TUIClient:
self._stats_cache = {}
self._stats_time = 0
# 鼠标点击区域: list of (y, page_id)
self._click_zones: list[tuple[int, str]] = []
def _fetch_stats(self) -> dict:
@@ -124,368 +111,6 @@ class TUIClient:
pass
return self._stats_cache
# ── 鼠标事件 ──────────────────────────────────────────
@staticmethod
def _parse_sgr_mouse(data: str):
"""解析 SGR 鼠标事件 \x1b[<button;x;y;M/m → (button, x, y)"""
m = re.match(r"^\x1b\[<(\d+);(\d+);(\d+)([Mm])$", data)
if m:
return int(m.group(1)), int(m.group(2)), int(m.group(3))
return None
# ── 屏幕渲染 ──────────────────────────────────────────
def _draw_header(self):
w = self.width
alive = backend_alive(self.host, self.port)
status_icon = "" if alive else ""
status_color = C["green"] if alive else C["red"]
print(bg(*C["header_bg"]), end="")
print(" " * w, end="")
print(f"\r{bg(*C['header_bg'])} ", end="")
title = " NebulaShell TUI "
print(fg(*C["accent"]) + bold(title) + rst(), end="")
print(bg(*C["header_bg"]), end="")
right = f" {fg(*status_color)}{status_icon}{rst()}{bg(*C['header_bg'])} {fg(*C['dim'])}{self.host}:{self.port}{rst()}"
print(f"\x1b[{w - len(right) + 1}G{right}", end="")
print(rst())
def _draw_status_bar(self):
w = self.width
print(bg(*C["status_bg"]), end="")
print(" " * w, end="")
print(f"\r{bg(*C['status_bg'])} ", end="")
nav_hint = f"{fg(*C['dim'])}数字/点击导航 q 退出 r 刷新{rst()}"
page_name = self.current_page.upper()
page_info = f"{fg(*C['cyan'])}{page_name}{rst()}"
print(f"\r{bg(*C['status_bg'])} {page_info}", end="")
print(f"\x1b[{w - len(nav_hint) + 1}G{nav_hint}", end="")
print(rst())
def _clear(self):
print("\x1b[2J\x1b[H", end="")
def _render_all(self):
self._click_zones.clear()
self._clear()
self._draw_header()
print()
content_top = 2 # header(1) + blank(1) = 2
alive = backend_alive(self.host, self.port)
if self.current_page == "welcome":
self._render_welcome(alive, content_top)
elif self.current_page == "dashboard":
self._render_dashboard()
elif self.current_page == "logs":
self._render_logs()
elif self.current_page == "terminal":
self._render_terminal()
elif self.current_page == "plugins":
self._render_plugins()
else:
self._render_home()
used = self._content_lines + 4
for _ in range(self.height - used):
print()
self._draw_status_bar()
sys.stdout.flush()
# ── 页面内容 ──────────────────────────────────────────
def _render_welcome(self, alive: bool, top: int):
w = self.width
self._content_lines = 0
logo = [
"███╗ ██╗███████╗██████╗ ██╗ ██╗██╗ █████╗ ",
"████╗ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔══██╗",
"██╔██╗ ██║█████╗ ██████╔╝██║ ██║██║ ███████║",
"██║╚██╗██║██╔══╝ ██╔══██╗██║ ██║██║ ██╔══██║",
"██║ ╚████║███████╗██████╔╝╚██████╔╝███████╗██║ ██║",
"╚═╝ ╚═══╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝",
]
for line in logo:
print(" " + fg(*C["accent"]) + line + rst())
self._content_lines += 1
print()
self._content_lines += 1
print(f" {fg(*C['dim'])}一切皆为插件的开发者工具运行时框架{rst()}")
self._content_lines += 1
print()
self._content_lines += 1
if alive:
print(f" {fg(*C['green'])}● 后端已连接{rst()} {fg(*C['dim'])}{self.base_url}{rst()}")
else:
print(f" {fg(*C['red'])}○ 后端未连接{rst()} {fg(*C['dim'])}{self.base_url}{rst()}")
self._content_lines += 1
print(f" {fg(*C['dim'])}请先启动后端: nebula serve{rst()}")
self._content_lines += 1
print()
self._content_lines += 1
print(f" {fg(*C['dim'])}─ 点击或按键导航 ─{rst()}")
self._content_lines += 1
for i, pg in enumerate(self.PAGES):
line_y = top + self._content_lines
self._click_zones.append((line_y, pg["id"]))
key = str(i + 1) if i < 9 else "0"
print(f" [{fg(*C['accent'])}{key}{rst()}] {bold(pg['label'])} {fg(*C['dim'])}{pg['desc']}{rst()}")
self._content_lines += 1
def _render_home(self):
w = self.width
self._content_lines = 0
stats = self._fetch_stats()
if not stats:
print(f" {fg(*C['dim'])}无法获取系统信息{rst()}")
self._content_lines += 1
return
uptime = stats.get("uptime", "N/A")
processes = stats.get("processes", 0)
cpu = stats.get("cpu", {})
mem = stats.get("ram", {})
disk = stats.get("disk", {})
print(f" {bold('系统概览')}")
self._content_lines += 1
print(f" {fg(*C['dim'])}运行时间: {uptime} 进程数: {processes}{rst()}")
self._content_lines += 1
print()
self._content_lines += 1
bar_w = w - 30
cpu_p = cpu.get("percent", 0)
print(f" CPU {fg(*C['dim'])}{str(cpu_p).rjust(5)}%{rst()} {hbar(bar_w, cpu_p, C['green'] if cpu_p < 50 else C['yellow'] if cpu_p < 80 else C['red'])}")
self._content_lines += 1
ram_p = mem.get("percent", 0)
ram_u = mem.get("used", 0)
ram_t = mem.get("total", 0)
print(f" 内存 {fg(*C['dim'])}{str(ram_p).rjust(5)}%{rst()} {hbar(bar_w, ram_p, C['green'] if ram_p < 50 else C['yellow'] if ram_p < 80 else C['red'])} {fg(*C['dim'])}{ram_u}G / {ram_t}G{rst()}")
self._content_lines += 1
disk_p = disk.get("percent", 0)
disk_u = disk.get("used", 0)
disk_t = disk.get("total", 0)
print(f" 磁盘 {fg(*C['dim'])}{str(disk_p).rjust(5)}%{rst()} {hbar(bar_w, disk_p, C['green'] if disk_p < 50 else C['yellow'] if disk_p < 80 else C['red'])} {fg(*C['dim'])}{disk_u}G / {disk_t}G{rst()}")
self._content_lines += 1
net = stats.get("network", {})
recv = net.get("recv_rate", 0)
sent = net.get("sent_rate", 0)
latency = stats.get("latency", 0)
print(f" 网络 {fg(*C['dim'])}{self._fmt_bytes(recv)}/s ▲ {self._fmt_bytes(sent)}/s 延迟: {latency}ms{rst()}")
self._content_lines += 1
load = stats.get("load", {})
l1 = load.get("load1", 0)
l5 = load.get("load5", 0)
l15 = load.get("load15", 0)
print(f" 负载 {fg(*C['dim'])}1m: {l1} 5m: {l5} 15m: {l15}{rst()}")
self._content_lines += 1
def _render_dashboard(self):
w = self.width
self._content_lines = 0
stats = self._fetch_stats()
if not stats:
print(f" {fg(*C['dim'])}无法获取仪表盘数据{rst()}")
self._content_lines += 1
return
print(f" {bold('系统仪表盘')} 实时监控")
self._content_lines += 1
cpu = stats.get("cpu", {})
mem = stats.get("ram", {})
disk = stats.get("disk", {})
net = stats.get("network", {})
disk_io = stats.get("disk_io", {})
load = stats.get("load", {})
latency = stats.get("latency", 0)
processes = stats.get("processes", 0)
uptime = stats.get("uptime", "N/A")
bar_w = w - 36
print()
self._content_lines += 1
cpu_p = cpu.get("percent", 0)
cpu_cores = cpu.get("cores", 0)
print(f" {fg(*C['cyan'])}CPU {rst()}{hbar(bar_w, cpu_p, C['green'] if cpu_p < 50 else C['yellow'] if cpu_p < 80 else C['red'])} {fg(*C['white'])}{cpu_p}%{rst()} {fg(*C['dim'])}({cpu_cores} 核){rst()}")
self._content_lines += 1
ram_p = mem.get("percent", 0)
ram_u = mem.get("used", 0)
ram_t = mem.get("total", 0)
print(f" {fg(*C['cyan'])}内存 {rst()}{hbar(bar_w, ram_p, C['green'] if ram_p < 50 else C['yellow'] if ram_p < 80 else C['red'])} {fg(*C['white'])}{ram_p}%{rst()} {fg(*C['dim'])}{ram_u}G / {ram_t}G{rst()}")
self._content_lines += 1
disk_p = disk.get("percent", 0)
disk_u = disk.get("used", 0)
disk_t = disk.get("total", 0)
print(f" {fg(*C['cyan'])}磁盘 {rst()}{hbar(bar_w, disk_p, C['green'] if disk_p < 50 else C['yellow'] if disk_p < 80 else C['red'])} {fg(*C['white'])}{disk_p}%{rst()} {fg(*C['dim'])}{disk_u}G / {disk_t}G{rst()}")
self._content_lines += 1
print()
self._content_lines += 1
recv = net.get("recv_rate", 0)
sent = net.get("sent_rate", 0)
tr = net.get("total_recv", 0)
ts = net.get("total_sent", 0)
print(f" {fg(*C['cyan'])}网络 {rst()}{fg(*C['green'])}{self._fmt_bytes(recv)}/s{rst()}{fg(*C['yellow'])}{self._fmt_bytes(sent)}/s{rst()} {fg(*C['dim'])}总量: {self._fmt_bytes(tr)} / {self._fmt_bytes(ts)}{rst()}")
self._content_lines += 1
disk_r = disk_io.get("read_rate", 0)
disk_w = disk_io.get("write_rate", 0)
print(f" {fg(*C['cyan'])}磁盘IO {rst()}{fg(*C['green'])}{self._fmt_bytes(disk_r)}/s{rst()}{fg(*C['yellow'])}{self._fmt_bytes(disk_w)}/s{rst()}")
self._content_lines += 1
print()
self._content_lines += 1
l1 = load.get("load1", 0)
l5 = load.get("load5", 0)
l15 = load.get("load15", 0)
print(f" {fg(*C['cyan'])}负载 {rst()}1m: {fg(*C['white'])}{l1}{rst()} 5m: {fg(*C['white'])}{l5}{rst()} 15m: {fg(*C['white'])}{l15}{rst()} 进程: {fg(*C['white'])}{processes}{rst()} 延迟: {fg(*C['white'])}{latency}ms{rst()}")
self._content_lines += 1
print(f" {fg(*C['cyan'])}运行 {rst()}{fg(*C['dim'])}{uptime}{rst()}")
self._content_lines += 1
def _render_logs(self):
self._content_lines = 0
print(f" {bold('系统日志')}")
self._content_lines += 1
print(f" {fg(*C['dim'])}实时日志输出(待实现){rst()}")
self._content_lines += 1
def _render_terminal(self):
self._content_lines = 0
print(f" {bold('终端')}")
self._content_lines += 1
print(f" {fg(*C['dim'])}Shell 终端(待实现){rst()}")
self._content_lines += 1
def _render_plugins(self):
self._content_lines = 0
print(f" {bold('插件管理')}")
self._content_lines += 1
print(f" {fg(*C['dim'])}插件列表(待实现){rst()}")
self._content_lines += 1
# ── 工具 ──────────────────────────────────────────────
@staticmethod
def _fmt_bytes(b):
if b > 1024**3:
return f"{b/(1024**3):.1f}G"
if b > 1024**2:
return f"{b/(1024**2):.1f}M"
if b > 1024:
return f"{b/1024:.1f}K"
return f"{b:.0f}B"
# ── 主循环 ────────────────────────────────────────────
def _navigate(self, page_id: str):
self._stats_cache = {}
self._stats_time = 0
self.current_page = page_id
self.width, self.height = term_size()
self._render_all()
def run(self):
self.width, self.height = term_size()
self.running = True
self._render_all()
signal.signal(signal.SIGWINCH, TUIClient._sigwinch)
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
# setraw 会关闭 ONLCR\n→\r\n重新开启避免阶梯乱码
attrs = termios.tcgetattr(fd)
attrs[1] = attrs[1] | termios.ONLCR
termios.tcsetattr(fd, termios.TCSANOW, attrs)
sys.stdout.write(_MOUSE_ON)
sys.stdout.flush()
buf = ""
while self.running:
# 终端 resize 检测
if TUIClient._resize_flag:
TUIClient._resize_flag = False
self.width, self.height = term_size()
self._render_all()
ch = sys.stdin.read(1)
buf += ch
# 检测 SGR 鼠标事件结束符 M/m
if buf.startswith("\x1b[<") and ch in ("M", "m"):
ev = self._parse_sgr_mouse(buf)
buf = ""
if ev:
button, mx, my = ev
if button == 0 and ch == "M": # 左键按下
for zy, page_id in self._click_zones:
if my == zy + 1: # 鼠标坐标 1-based
self._navigate(page_id)
break
continue
# 非鼠标序列 → 重置缓冲区
if not buf.startswith("\x1b"):
pass
elif buf.startswith("\x1b[<"):
continue # 等待更多字符
elif len(buf) > 1:
buf = "" # 其他转义序列,丢弃
# 处理单字符输入
if len(buf) == 1:
c = buf
buf = ""
if c in ("q", "Q", "\x03", "\x04"):
break
elif c == "1":
self._navigate("welcome")
elif c == "2":
self._navigate("dashboard")
elif c == "3":
self._navigate("logs")
elif c == "4":
self._navigate("terminal")
elif c == "5":
self._navigate("plugins")
elif c in ("r", "R"):
self._stats_cache = {}
self._stats_time = 0
self._render_all()
except Exception:
pass
finally:
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
sys.stdout.write(_MOUSE_OFF)
sys.stdout.flush()
termios.tcsetattr(fd, termios.TCSADRAIN, old)
print("\x1b[2J\x1b[H\x1b[0mTUI 已退出\n")