品牌标识与握手诊断

- 向导开场:像素字符画 Logo(❯_ 提示符,青绿+光标绿)+ 版本/平台/配置信息 + 横线
- 运行面板头部加 ❯_ 标识;连接中状态改为 ◐◓◑◒ 旋转动画
- SSH banner 错误改为可读诊断文案;握手超时 12→25s;重连日志带尝试次数
This commit is contained in:
leefer
2026-09-27 20:57:46 +08:00
parent 5858656f62
commit 7229642c62
3 changed files with 67 additions and 15 deletions
+20 -8
View File
@@ -25,6 +25,25 @@ from engine import TunnelManager, probe_host # noqa: E402
from storage import Store, data_root # noqa: E402
import tui # noqa: E402
VERSION = "1.4"
def _print_banner(store: Store) -> None:
"""开场:像素 Logo + 产品信息(类 Claude Code 风格)。"""
from platform import python_version, system
logo = tui.logo_lines()
info = [
tui.bold("ssh-e2e · SSH 隧道管理器"),
tui.dim(f"v{VERSION} · 中文交互向导"),
tui.faint(f"{system()} · Python {python_version()}"),
tui.faint(f"配置 {store.path}"),
tui.faint("回车采用 [默认值] · Ctrl+C 退出"),
]
print()
for row, line in zip(logo, info):
print(f" {row} {line}")
print(" " + tui.accent("─" * 46))
def _print(msg: str) -> None:
print(msg, flush=True)
@@ -392,14 +411,7 @@ def _wizard_new_tunnel(store: Store, host: dict):
def wizard(store: Store) -> int:
_ensure_tty_stdin()
title = "SSH 隧道配置向导"
sub = "回车采用 [方括号] 默认值 · Ctrl+C 随时退出"
w = max(tui.disp_len(title) + 4, tui.disp_len(sub) + 4, 40)
print()
print(tui.accent("╭─ ") + tui.bold(title) + " " + tui.accent("─" * (w - tui.disp_len(title) - 3) + "╮"))
print(tui.accent("│") + " " + tui.faint(sub) + " " * (w - tui.disp_len(sub) - 2) + tui.accent("│"))
print(tui.accent("╰" + "─" * w + "╯"))
print(tui.faint(f" 配置文件: {store.path}"))
_print_banner(store)
if store.warn_plaintext:
_print("提示: 当前平台密码以明文保存于配置文件(权限已设为仅本用户可读)。")
+9 -2
View File
@@ -20,6 +20,9 @@ def friendly_error(e: Exception) -> str:
return "SSH 认证失败:请检查用户名、密码或私钥。"
if isinstance(e, paramiko.SSHException):
msg = str(e)
if "banner" in msg.lower():
return ("SSH 握手失败:已连上端口但对方未回应 SSH 协议"
"(远程主机宕机/过载,或端口映射网关异常)")
if "negotiation" in msg or "timed out" in msg or "Timeout" in msg:
return "SSH 握手超时:远程主机无响应。"
if "not a valid" in msg.lower() or ("key" in msg.lower() and "invalid" in msg.lower()):
@@ -59,6 +62,7 @@ class Tunnel:
self._client: paramiko.SSHClient | None = None
self._listener: socket.socket | None = None
self._wakeup = threading.Event()
self.attempts = 0 # 本次断线后的重试次数
# ---------- 状态 ----------
@@ -168,7 +172,7 @@ class Tunnel:
port = int(host.get("port") or 22)
kw = dict(
hostname=host["hostname"], port=port, username=host.get("username"),
timeout=12, banner_timeout=12, auth_timeout=12,
timeout=15, banner_timeout=25, auth_timeout=15,
allow_agent=False, look_for_keys=False,
)
if host.get("auth") == "key":
@@ -177,8 +181,11 @@ class Tunnel:
kw["passphrase"] = host["key_passphrase"]
else:
kw["password"] = host.get("password")
self._log("info", f"正在连接 {host.get('username')}@{host['hostname']}:{port} ...")
self.attempts += 1
n = f"(第 {self.attempts} 次尝试)" if self.attempts > 1 else ""
self._log("info", f"正在连接 {host.get('username')}@{host['hostname']}:{port} ... {n}")
client.connect(**kw)
self.attempts = 0
try:
client.save_host_keys(str(self.known_hosts_path))
except Exception:
+38 -5
View File
@@ -69,6 +69,32 @@ def warn(s: str) -> str: return _c(s, AMBER)
def bold(s: str) -> str: return _c(s, TEXT, bold=True)
# ---------- 品牌标识:像素字符画 "❯_"(终端提示符) ----------
_LOGO_ROWS = [
"█.........",
"██........",
"█.█.......",
"██........",
"█...██████",
]
_CURSOR_FROM = 4 # 第 4 列起是"光标下划线",用绿色
def logo_lines() -> list:
"""5 行着色 logo;无色模式输出单色块画。"""
out = []
for row in _LOGO_ROWS:
line = ""
for i, ch in enumerate(row):
if ch == "█":
line += _c("█", GREEN if i >= _CURSOR_FROM else ACCENT)
else:
line += " "
out.append(line)
return out
# ---------- 显示宽度(CJK 全角按 2 列) ----------
_WIDE = ((0x1100, 0x115F), (0x2E80, 0x303E), (0x3041, 0x33FF), (0x3400, 0x4DBF),
@@ -225,6 +251,9 @@ _STATUS_STYLE = {
}
_SPIN = "◐◓◑◒" # 连接中旋转帧,按时间取模
class Dashboard:
"""运行时面板:隧道状态 + 滚动日志,原地刷新。非 tty 时为空操作。"""
@@ -256,16 +285,20 @@ class Dashboard:
if not self._on:
return
lines = []
head = " ssh-e2e · 隧道运行中 "
head = " ❯_ ssh-e2e · 隧道运行中 "
lines.append(accent("─" + head) + faint("─" * max(0, 46 - disp_len(head))))
for st in self._stats:
rgb, label = _STATUS_STYLE.get(st["status"], (FAINT, "○ 未知"))
dot = _c(label.split()[0], rgb)
text = " ".join(label.split()[1:])
if st["status"] == "connecting":
glyph = _SPIN[int(time.time() * 2) % len(_SPIN)]
dot, text = _c(glyph, AMBER), "连接中"
else:
rgb, label = _STATUS_STYLE.get(st["status"], (FAINT, "○ 未知"))
dot = _c(label.split()[0], rgb)
text = " ".join(label.split()[1:])
lines.append(f" {dot} {bold(st['name'])}")
m = f"{dim('127.0.0.1:' + str(st['local_port']))} {accent('⇄')} {dim(st['remote'])}"
uptime = fmt_up(time.time() - st["started_at"]) if st.get("started_at") else "--:--:--"
lines.append(f" {m} {_c(text, rgb)} · {faint(uptime)} · {faint('连接 ' + str(st.get('conns', 0)))}")
lines.append(f" {m} {_c(text, AMBER if st['status'] == 'connecting' else _STATUS_STYLE[st['status']][0])} · {faint(uptime)} · {faint('连接 ' + str(st.get('conns', 0)))}")
if st.get("error"):
lines.append(f" {err(st['error'])}")
if self._logs: