989 lines
37 KiB
Python
989 lines
37 KiB
Python
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from cli.tui.copy import TUI_COPY
|
|
from cli.tui.diagnostics import TUI_DIAGNOSTIC_SERVICE
|
|
from cli.tui.keymaps import TUI_KEYMAP_REGISTRY
|
|
from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER
|
|
|
|
DEPENDENCY_CHECK_TOTAL = 3
|
|
DASHBOARD_PANEL_MAX_LINES = 8
|
|
DASHBOARD_PANEL_LINE_MAX_LENGTH = 38
|
|
DASHBOARD_PANEL_MORE_HINT = TUI_COPY.build_more_detail_hint()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DashboardPanelSnapshot:
|
|
"""
|
|
TUI 首页单个面板快照。
|
|
|
|
:param title: 面板标题
|
|
:param status: 面板状态
|
|
:param lines: 面板正文文本行
|
|
"""
|
|
|
|
title: str
|
|
status: str
|
|
lines: list[str]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DashboardMetricSnapshot:
|
|
"""
|
|
TUI 首页单个指标卡快照。
|
|
|
|
:param title: 指标标题
|
|
:param value: 指标主值
|
|
:param status: 指标状态
|
|
:param hint: 指标补充说明
|
|
"""
|
|
|
|
title: str
|
|
value: str
|
|
status: str
|
|
hint: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DashboardSnapshot:
|
|
"""
|
|
TUI 首页聚合快照。
|
|
|
|
:param env: 当前运行环境
|
|
:param panels: 面板列表
|
|
"""
|
|
|
|
env: str
|
|
panels: list[DashboardPanelSnapshot]
|
|
metrics: list[DashboardMetricSnapshot] = field(default_factory=list)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DashboardSourcePayloads:
|
|
"""
|
|
TUI 首页聚合数据源快照。
|
|
|
|
:param app_env_payload: 应用环境结果
|
|
:param app_routes_payload: 应用路由结果
|
|
:param doctor_payload: 应用检查结果
|
|
:param database_payload: 数据库版本结果
|
|
:param cache_payload: 缓存统计结果
|
|
:param deps_payload: 依赖检查结果
|
|
:param server_payload: 服务器摘要结果
|
|
"""
|
|
|
|
app_env_payload: dict[str, Any] | None
|
|
app_routes_payload: dict[str, Any] | None
|
|
doctor_payload: dict[str, Any] | None
|
|
database_payload: dict[str, Any] | None
|
|
cache_payload: dict[str, Any] | None
|
|
deps_payload: dict[str, Any] | None
|
|
server_payload: dict[str, Any] | None
|
|
|
|
|
|
class DashboardSnapshotCollector:
|
|
"""
|
|
TUI 首页聚合数据采集器。
|
|
|
|
该对象负责从多路 CLI 命令收集 dashboard 所需原始结果,
|
|
让 `DashboardAdapter` 保持首页装配职责,而不继续承载所有采集细节。
|
|
"""
|
|
|
|
def collect(self, env: str) -> DashboardSourcePayloads:
|
|
"""
|
|
采集首页聚合所需的多路原始结果。
|
|
|
|
:param env: 当前运行环境
|
|
:return: 首页聚合数据源快照
|
|
"""
|
|
return DashboardSourcePayloads(
|
|
app_env_payload=NESTED_CLI_SUPPORT.run(
|
|
'app',
|
|
'env',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
app_routes_payload=NESTED_CLI_SUPPORT.run(
|
|
'app',
|
|
'routes',
|
|
f'--env={env}',
|
|
'--group-by=tag',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
doctor_payload=NESTED_CLI_SUPPORT.run(
|
|
'app',
|
|
'doctor',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
database_payload=NESTED_CLI_SUPPORT.run(
|
|
'db',
|
|
'current',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
cache_payload=NESTED_CLI_SUPPORT.run(
|
|
'cache',
|
|
'stats',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
deps_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'deps',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
server_payload=NESTED_CLI_SUPPORT.run(
|
|
'ops',
|
|
'server-info',
|
|
f'--env={env}',
|
|
'--output=json',
|
|
parse_json=True,
|
|
).payload,
|
|
)
|
|
|
|
|
|
class DashboardFormattingSupport:
|
|
"""
|
|
首页仪表盘格式化支持对象。
|
|
|
|
该对象负责统一处理摘要消息提取、状态优先级、布尔文本、信号条和
|
|
面板空态/失败态文案,供面板与指标构建对象共享。
|
|
"""
|
|
|
|
@staticmethod
|
|
def truncate_dashboard_line(line: str) -> str:
|
|
"""
|
|
截断首页面板单行文本,并保留结构化前缀。
|
|
|
|
:param line: 原始文本行
|
|
:return: 截断后的文本行
|
|
"""
|
|
stripped = str(line).strip()
|
|
if not stripped:
|
|
return ''
|
|
if stripped.startswith('## '):
|
|
return f'## {SHELL_TEXT_FORMATTER.truncate_text(stripped[3:].strip(), DASHBOARD_PANEL_LINE_MAX_LENGTH - 3)}'
|
|
if stripped.startswith('> '):
|
|
return f'> {SHELL_TEXT_FORMATTER.truncate_text(stripped[2:].strip(), DASHBOARD_PANEL_LINE_MAX_LENGTH - 2)}'
|
|
return SHELL_TEXT_FORMATTER.truncate_text(stripped, DASHBOARD_PANEL_LINE_MAX_LENGTH)
|
|
|
|
@staticmethod
|
|
def resolve_status_priority(status: str) -> int:
|
|
"""
|
|
解析状态优先级,值越小越需要优先关注。
|
|
|
|
:param status: 状态文本
|
|
:return: 优先级
|
|
"""
|
|
return {
|
|
'fail': 0,
|
|
'warn': 1,
|
|
'ok': 2,
|
|
'info': 3,
|
|
}.get(str(status).strip().lower(), 9)
|
|
|
|
@staticmethod
|
|
def render_bool_flag(value: object) -> str:
|
|
"""
|
|
将布尔语义值标准化为文本。
|
|
|
|
:param value: 原始值
|
|
:return: 规范化后的文本
|
|
"""
|
|
return '是' if bool(value) else '否'
|
|
|
|
@staticmethod
|
|
def render_signal_bar(
|
|
passed: int,
|
|
total: int,
|
|
*,
|
|
width: int = 8,
|
|
on: str = '#',
|
|
off: str = '-',
|
|
) -> str:
|
|
"""
|
|
将通过数量渲染为 ASCII 信号条。
|
|
|
|
:param passed: 已通过数量
|
|
:param total: 总数量
|
|
:param width: 信号条宽度
|
|
:param on: 点亮字符
|
|
:param off: 熄灭字符
|
|
:return: ASCII 信号条
|
|
"""
|
|
if total <= 0:
|
|
return f'[{off * width}]'
|
|
safe_passed = max(0, min(passed, total))
|
|
filled = round((safe_passed / total) * width)
|
|
return f'[{on * filled}{off * max(0, width - filled)}]'
|
|
|
|
@staticmethod
|
|
def extract_payload_message(payload: dict[str, Any] | None) -> str:
|
|
"""
|
|
提取结果负载中的摘要消息。
|
|
|
|
:param payload: 标准结果负载
|
|
:return: 摘要文本
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return '-'
|
|
if payload.get('message'):
|
|
return str(payload.get('message'))
|
|
if payload.get('error'):
|
|
return SHELL_TEXT_FORMATTER.truncate_text(payload.get('error'), 120)
|
|
return '-'
|
|
|
|
def build_failure_lines(
|
|
self,
|
|
payload: dict[str, Any] | None,
|
|
*,
|
|
summary_label: str,
|
|
summary_value: str,
|
|
suggestion: str = TUI_COPY.build_dashboard_failure_suggestion(),
|
|
) -> list[str]:
|
|
"""
|
|
构建首页面板统一失败态摘要。
|
|
|
|
:param payload: 标准结果负载
|
|
:param summary_label: 主字段名称
|
|
:param summary_value: 主字段值
|
|
:param suggestion: 建议操作
|
|
:return: 面板正文
|
|
"""
|
|
return [
|
|
'## 当前状态',
|
|
f'{summary_label}: {summary_value}',
|
|
'',
|
|
'## 错误摘要',
|
|
f'> {self.extract_payload_message(payload)}',
|
|
'',
|
|
'## 建议操作',
|
|
suggestion,
|
|
]
|
|
|
|
@staticmethod
|
|
def build_empty_lines(
|
|
*,
|
|
summary_label: str,
|
|
summary_value: str,
|
|
detail: str,
|
|
suggestion: str = TUI_COPY.build_dashboard_empty_suggestion(),
|
|
) -> list[str]:
|
|
"""
|
|
构建首页面板统一空态摘要。
|
|
|
|
:param summary_label: 主字段名称
|
|
:param summary_value: 主字段值
|
|
:param detail: 空态说明
|
|
:param suggestion: 建议操作
|
|
:return: 面板正文
|
|
"""
|
|
return [
|
|
'## 当前状态',
|
|
f'{summary_label}: {summary_value}',
|
|
'',
|
|
'## 说明',
|
|
detail,
|
|
'',
|
|
'## 建议操作',
|
|
suggestion,
|
|
]
|
|
|
|
|
|
class DashboardPanelCompressor:
|
|
"""
|
|
首页面板压缩器。
|
|
|
|
该对象负责将原始多行面板压缩为适合首页展示的统一密度摘要卡。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
"""
|
|
|
|
def __init__(self, formatting: DashboardFormattingSupport) -> None:
|
|
"""
|
|
初始化首页面板压缩器。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
:return: None
|
|
"""
|
|
self.formatting = formatting
|
|
|
|
def compact_panel_lines(self, lines: list[str]) -> list[str]:
|
|
"""
|
|
将首页面板正文压缩为统一长度的摘要卡内容。
|
|
|
|
:param lines: 原始正文文本行
|
|
:return: 压缩后的摘要行列表
|
|
"""
|
|
compact_lines: list[str] = []
|
|
for raw_line in lines:
|
|
compact_line = self.formatting.truncate_dashboard_line(raw_line)
|
|
if not compact_line:
|
|
if compact_lines and compact_lines[-1] != '':
|
|
compact_lines.append('')
|
|
continue
|
|
compact_lines.append(compact_line)
|
|
|
|
while compact_lines and compact_lines[0] == '':
|
|
compact_lines.pop(0)
|
|
while compact_lines and compact_lines[-1] == '':
|
|
compact_lines.pop()
|
|
|
|
if len(compact_lines) <= DASHBOARD_PANEL_MAX_LINES:
|
|
return compact_lines
|
|
|
|
summarized_lines = compact_lines[: DASHBOARD_PANEL_MAX_LINES - 1]
|
|
while summarized_lines and summarized_lines[-1] == '':
|
|
summarized_lines.pop()
|
|
summarized_lines.append(DASHBOARD_PANEL_MORE_HINT)
|
|
return summarized_lines
|
|
|
|
def compact_panels(self, panels: list[DashboardPanelSnapshot]) -> list[DashboardPanelSnapshot]:
|
|
"""
|
|
将首页所有面板压缩为统一密度的摘要卡。
|
|
|
|
:param panels: 原始面板列表
|
|
:return: 压缩后的面板列表
|
|
"""
|
|
return [
|
|
DashboardPanelSnapshot(
|
|
title=panel.title,
|
|
status=panel.status,
|
|
lines=self.compact_panel_lines(panel.lines),
|
|
)
|
|
for panel in panels
|
|
]
|
|
|
|
|
|
class DashboardPanelBuilder:
|
|
"""
|
|
首页面板构建器。
|
|
|
|
该对象负责根据各个 CLI 结果负载构建首页展示面板,并统一风险热区与
|
|
建议入口逻辑。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
"""
|
|
|
|
def __init__(self, formatting: DashboardFormattingSupport) -> None:
|
|
"""
|
|
初始化首页面板构建器。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
:return: None
|
|
"""
|
|
self.formatting = formatting
|
|
|
|
def build_app_env_panel(
|
|
self,
|
|
env: str,
|
|
env_payload: dict[str, Any] | None,
|
|
routes_payload: dict[str, Any] | None,
|
|
) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建应用环境总览面板。
|
|
|
|
:param env: 当前运行环境
|
|
:param env_payload: `app env` JSON 负载
|
|
:param routes_payload: `app routes` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
runtime = env_payload.get('runtime') if isinstance(env_payload, dict) else None
|
|
route_count = routes_payload.get('count', 0) if isinstance(routes_payload, dict) else 0
|
|
grouped_routes = routes_payload.get('groupedRoutes') if isinstance(routes_payload, dict) else None
|
|
group_count = len(grouped_routes) if isinstance(grouped_routes, dict) else 0
|
|
if not isinstance(runtime, dict):
|
|
return DashboardPanelSnapshot(
|
|
title='应用摘要',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
env_payload,
|
|
summary_label='环境',
|
|
summary_value=env,
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('应用', '检查环境解析结果'),
|
|
),
|
|
)
|
|
return DashboardPanelSnapshot(
|
|
title='应用摘要',
|
|
status='ok' if env_payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 环境映射',
|
|
f'当前环境: {env} | CLI 目标环境: {runtime.get("cliEnv", "-")}',
|
|
f'配置文件环境: {runtime.get("configEnv", "-")}',
|
|
'## 路由摘要',
|
|
f'注册路由: {route_count} 条 | 标签分组: {group_count} 个',
|
|
'## 环境文件',
|
|
f'已加载环境文件: {runtime.get("envFile", "-")}',
|
|
f'环境文件存在: {self.formatting.render_bool_flag(runtime.get("envFileExists", False))}',
|
|
],
|
|
)
|
|
|
|
def build_health_panel(self, payload: dict[str, Any] | None) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建健康检查面板。
|
|
|
|
:param payload: `app doctor` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DashboardPanelSnapshot(
|
|
title='系统摘要',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
payload,
|
|
summary_label='巡检结果',
|
|
summary_value='不可用',
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('运维/健康检查', '查看原因'),
|
|
),
|
|
)
|
|
|
|
database = payload.get('database') if isinstance(payload.get('database'), dict) else {}
|
|
redis = payload.get('redis') if isinstance(payload.get('redis'), dict) else {}
|
|
crypto = payload.get('crypto') if isinstance(payload.get('crypto'), dict) else {}
|
|
return DashboardPanelSnapshot(
|
|
title='系统摘要',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 总体态势',
|
|
f'整体结果: {"通过" if payload.get("ok", False) else "异常"}',
|
|
'',
|
|
'## 依赖检查',
|
|
f'数据库: {"正常" if database.get("ok", False) else "异常"}',
|
|
f'> {self.formatting.extract_payload_message(database)}',
|
|
f'Redis: {"正常" if redis.get("ok", False) else "异常"}',
|
|
f'> {self.formatting.extract_payload_message(redis)}',
|
|
f'加密组件: {"正常" if crypto.get("ok", False) else "异常"}',
|
|
f'> {self.formatting.extract_payload_message(crypto)}',
|
|
],
|
|
)
|
|
|
|
def build_recommended_entry_panel(
|
|
self,
|
|
doctor_payload: dict[str, Any] | None,
|
|
database_payload: dict[str, Any] | None,
|
|
cache_payload: dict[str, Any] | None,
|
|
) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建首页建议入口面板。
|
|
|
|
:param doctor_payload: 健康检查负载
|
|
:param database_payload: 数据库状态负载
|
|
:param cache_payload: 缓存状态负载
|
|
:return: 面板快照
|
|
"""
|
|
database_ok = (
|
|
bool(doctor_payload.get('database', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
)
|
|
redis_ok = bool(doctor_payload.get('redis', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
crypto_ok = (
|
|
bool(doctor_payload.get('crypto', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
)
|
|
db_revision = str(database_payload.get('currentRevision', '-') if isinstance(database_payload, dict) else '-')
|
|
cache_size = cache_payload.get('dbSize', '-') if isinstance(cache_payload, dict) else '-'
|
|
|
|
if not database_ok:
|
|
status = 'fail'
|
|
lines = [
|
|
'## 首选入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("database").upper()}] 数据库',
|
|
f'> {TUI_DIAGNOSTIC_SERVICE.build_database_focus_hint()}',
|
|
'',
|
|
'## 次级入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("ops").upper()}] 运维',
|
|
f'> {TUI_DIAGNOSTIC_SERVICE.build_ops_focus_hint()}',
|
|
'',
|
|
'## 当前判断',
|
|
f'迁移版本: {db_revision}',
|
|
'> 数据库异常会优先阻断后续业务巡检',
|
|
]
|
|
elif not redis_ok:
|
|
status = 'fail'
|
|
lines = [
|
|
'## 首选入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("cache").upper()}] 缓存',
|
|
f'> {TUI_DIAGNOSTIC_SERVICE.build_cache_focus_hint()}',
|
|
'',
|
|
'## 次级入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("ops").upper()}] 运维',
|
|
f'> {TUI_DIAGNOSTIC_SERVICE.build_ops_focus_hint()}',
|
|
'',
|
|
'## 当前判断',
|
|
f'Redis 键数: {cache_size}',
|
|
'> 缓存异常通常会影响配置读取和任务运行',
|
|
]
|
|
elif not crypto_ok:
|
|
status = 'warn'
|
|
lines = [
|
|
'## 首选入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("crypto").upper()}] 加密',
|
|
'> 先检查运行校验、公钥身份和兼容版本',
|
|
'',
|
|
'## 次级入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("ops").upper()}] 运维',
|
|
'> 对照依赖版本,确认是否缺少加密相关运行依赖',
|
|
'',
|
|
'## 当前判断',
|
|
'> 加密异常不会立即阻断全部能力,但会影响安全链路',
|
|
]
|
|
else:
|
|
status = 'ok'
|
|
lines = [
|
|
'## 首选入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("jobs").upper()}] 任务 · {TUI_DIAGNOSTIC_SERVICE.build_jobs_focus_hint()}',
|
|
'## 次级入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("configs").upper()}] 参数配置 · {TUI_DIAGNOSTIC_SERVICE.build_configs_focus_hint()}',
|
|
'## 扩展入口',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("gen").upper()}] 代码生成 · {TUI_DIAGNOSTIC_SERVICE.build_gen_focus_hint()}',
|
|
f'[{TUI_KEYMAP_REGISTRY.get_navigation_shortcut("app").upper()}] 应用 · 配置摘要 / 路由状态',
|
|
]
|
|
|
|
return DashboardPanelSnapshot(
|
|
title='建议摘要',
|
|
status=status,
|
|
lines=lines,
|
|
)
|
|
|
|
def build_inspection_conclusion_panel(
|
|
self,
|
|
doctor_payload: dict[str, Any] | None,
|
|
database_payload: dict[str, Any] | None,
|
|
cache_payload: dict[str, Any] | None,
|
|
) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建首页巡检结论面板。
|
|
|
|
:param doctor_payload: 健康检查负载
|
|
:param database_payload: 数据库状态负载
|
|
:param cache_payload: 缓存状态负载
|
|
:return: 面板快照
|
|
"""
|
|
database_ok = (
|
|
bool(doctor_payload.get('database', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
)
|
|
redis_ok = bool(doctor_payload.get('redis', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
crypto_ok = (
|
|
bool(doctor_payload.get('crypto', {}).get('ok', False)) if isinstance(doctor_payload, dict) else False
|
|
)
|
|
db_revision = database_payload.get('currentRevision', '-') if isinstance(database_payload, dict) else '-'
|
|
cache_size = cache_payload.get('dbSize', '-') if isinstance(cache_payload, dict) else '-'
|
|
|
|
if not database_ok:
|
|
status = 'fail'
|
|
conclusion = '数据库存在异常,建议优先处理连接或迁移问题'
|
|
next_step = '优先进入数据库页面,确认迁移版本和连接配置'
|
|
elif not redis_ok:
|
|
status = 'fail'
|
|
conclusion = '缓存服务存在异常,建议优先检查 Redis 可用性'
|
|
next_step = '优先进入缓存页面,确认连接数、键数量和状态说明'
|
|
elif not crypto_ok:
|
|
status = 'warn'
|
|
conclusion = '加密组件状态异常,建议尽快核对运行依赖'
|
|
next_step = '先查看系统摘要面板,再排查加密组件依赖'
|
|
else:
|
|
status = 'ok'
|
|
conclusion = '当前核心依赖状态正常,可继续查看业务分区'
|
|
next_step = '优先查看任务与参数配置页面,确认业务侧是否有风险'
|
|
|
|
return DashboardPanelSnapshot(
|
|
title='总览判断',
|
|
status=status,
|
|
lines=[
|
|
'## 当前结论',
|
|
conclusion,
|
|
'',
|
|
'## 建议操作',
|
|
next_step,
|
|
'',
|
|
'## 当前快照',
|
|
f'迁移版本: {db_revision}',
|
|
f'Redis 键数: {cache_size}',
|
|
],
|
|
)
|
|
|
|
def build_database_panel(self, payload: dict[str, Any] | None) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建数据库状态面板。
|
|
|
|
:param payload: `db current` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
current_revision = payload.get('currentRevision', '-') if isinstance(payload, dict) else '-'
|
|
if not isinstance(payload, dict) or not payload.get('ok', False):
|
|
return DashboardPanelSnapshot(
|
|
title='数据库摘要',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
payload,
|
|
summary_label='迁移版本',
|
|
summary_value=str(current_revision or '-'),
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('数据库', '确认迁移版本与连接状态'),
|
|
),
|
|
)
|
|
return DashboardPanelSnapshot(
|
|
title='数据库摘要',
|
|
status='ok',
|
|
lines=[
|
|
'## 迁移信息',
|
|
f'迁移版本: {current_revision}',
|
|
'',
|
|
'## 状态说明',
|
|
f'> {self.formatting.extract_payload_message(payload)}',
|
|
],
|
|
)
|
|
|
|
def build_cache_panel(self, payload: dict[str, Any] | None) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建缓存状态面板。
|
|
|
|
:param payload: `cache stats` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DashboardPanelSnapshot(
|
|
title='缓存摘要',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
payload,
|
|
summary_label='缓存状态',
|
|
summary_value='不可用',
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('缓存', '查看 Redis 状态'),
|
|
),
|
|
)
|
|
info = payload.get('info') if isinstance(payload.get('info'), dict) else {}
|
|
cache_names = payload.get('cacheNames') if isinstance(payload.get('cacheNames'), list) else []
|
|
return DashboardPanelSnapshot(
|
|
title='缓存摘要',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## Redis 概况',
|
|
f'当前键数: {payload.get("dbSize", "-")}',
|
|
f'Redis 版本: {info.get("redis_version", "-")}',
|
|
f'客户端连接数: {info.get("connected_clients", "-")}',
|
|
'',
|
|
'## 缓存资源',
|
|
f'已登记缓存名: {len(cache_names)} 个',
|
|
],
|
|
)
|
|
|
|
def build_dependency_panel(self, payload: dict[str, Any] | None) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建首页依赖版本摘要面板。
|
|
|
|
:param payload: `ops deps` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
if not isinstance(payload, dict):
|
|
return DashboardPanelSnapshot(
|
|
title='依赖版本',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
payload,
|
|
summary_label='依赖检查',
|
|
summary_value='不可用',
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('运维', '检查依赖详情'),
|
|
),
|
|
)
|
|
packages = payload.get('packages') if isinstance(payload.get('packages'), dict) else {}
|
|
missing_required = payload.get('missingRequired') if isinstance(payload.get('missingRequired'), list) else []
|
|
lines = [
|
|
'## 依赖结论',
|
|
f'检查结果: {"通过" if payload.get("ok", False) else "异常"}',
|
|
f'缺失核心依赖: {len(missing_required)} 个',
|
|
'',
|
|
'## 关键版本',
|
|
]
|
|
for package_name in ('python', 'fastapi', 'sqlalchemy', 'redis'):
|
|
package_payload = packages.get(package_name)
|
|
if not isinstance(package_payload, dict):
|
|
continue
|
|
installed = '已安装' if package_payload.get('installed', False) else '缺失'
|
|
lines.append(f'{package_name}: {installed} · {package_payload.get("version", "-") or "-"}')
|
|
return DashboardPanelSnapshot(
|
|
title='依赖版本',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=lines,
|
|
)
|
|
|
|
def build_server_info_panel(self, payload: dict[str, Any] | None) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建首页服务器概况面板。
|
|
|
|
:param payload: `ops server-info` JSON 负载
|
|
:return: 面板快照
|
|
"""
|
|
server = payload.get('server') if isinstance(payload, dict) else None
|
|
if not isinstance(server, dict):
|
|
return DashboardPanelSnapshot(
|
|
title='服务器摘要',
|
|
status='fail',
|
|
lines=self.formatting.build_failure_lines(
|
|
payload,
|
|
summary_label='服务器摘要',
|
|
summary_value='不可用',
|
|
suggestion=TUI_COPY.build_dashboard_page_suggestion('运维', '重新采集服务器摘要'),
|
|
),
|
|
)
|
|
sys_info = server.get('sys') if isinstance(server.get('sys'), dict) else {}
|
|
cpu_info = server.get('cpu') if isinstance(server.get('cpu'), dict) else {}
|
|
mem_info = server.get('mem') if isinstance(server.get('mem'), dict) else {}
|
|
return DashboardPanelSnapshot(
|
|
title='服务器摘要',
|
|
status='ok' if payload.get('ok', False) else 'fail',
|
|
lines=[
|
|
'## 主机状态',
|
|
f'主机名: {sys_info.get("computerName", "-")}',
|
|
f'主机 IP: {sys_info.get("computerIp", "-")}',
|
|
'',
|
|
'## 资源概况',
|
|
f'CPU 使用率: {cpu_info.get("used", "-")}%',
|
|
f'内存使用率: {mem_info.get("usage", "-")}%',
|
|
],
|
|
)
|
|
|
|
def build_risk_heatmap_panel(self, panels: list[DashboardPanelSnapshot]) -> DashboardPanelSnapshot:
|
|
"""
|
|
构建首页风险热区面板。
|
|
|
|
:param panels: 当前已构建的面板列表
|
|
:return: 风险热区面板
|
|
"""
|
|
sorted_panels = sorted(
|
|
panels, key=lambda item: (self.formatting.resolve_status_priority(item.status), item.title)
|
|
)
|
|
fail_panels = [panel for panel in sorted_panels if panel.status == 'fail']
|
|
warn_panels = [panel for panel in sorted_panels if panel.status == 'warn']
|
|
primary_targets = fail_panels[:3] or warn_panels[:3] or sorted_panels[:3]
|
|
|
|
lines = [
|
|
'## 热区摘要',
|
|
f'失败面板: {len(fail_panels)} 个',
|
|
f'警告面板: {len(warn_panels)} 个',
|
|
'',
|
|
'## 优先关注',
|
|
]
|
|
if not primary_targets:
|
|
lines.extend(
|
|
self.formatting.build_empty_lines(
|
|
summary_label='风险热区',
|
|
summary_value='0 个',
|
|
detail='当前没有需要优先处理的失败或警告面板',
|
|
suggestion='可继续浏览任务、缓存、参数配置等业务页面',
|
|
)
|
|
)
|
|
else:
|
|
for index, panel in enumerate(primary_targets, start=1):
|
|
lead_line = SHELL_TEXT_FORMATTER.truncate_text(
|
|
panel.lines[1] if len(panel.lines) > 1 else panel.lines[0], 56
|
|
)
|
|
lines.extend(
|
|
[
|
|
f'> HOT-{index:02d} [{panel.status.upper()}] {panel.title}',
|
|
f'> {lead_line}',
|
|
]
|
|
)
|
|
|
|
status = 'fail' if fail_panels else 'warn' if warn_panels else 'ok'
|
|
return DashboardPanelSnapshot(
|
|
title='风险摘要',
|
|
status=status,
|
|
lines=lines,
|
|
)
|
|
|
|
def build_panels(
|
|
self,
|
|
*,
|
|
env: str,
|
|
app_env_payload: dict[str, Any] | None,
|
|
app_routes_payload: dict[str, Any] | None,
|
|
doctor_payload: dict[str, Any] | None,
|
|
database_payload: dict[str, Any] | None,
|
|
cache_payload: dict[str, Any] | None,
|
|
deps_payload: dict[str, Any] | None,
|
|
server_payload: dict[str, Any] | None,
|
|
) -> list[DashboardPanelSnapshot]:
|
|
"""
|
|
构建首页全部原始面板。
|
|
|
|
:param env: 当前运行环境
|
|
:param app_env_payload: 应用环境负载
|
|
:param app_routes_payload: 应用路由负载
|
|
:param doctor_payload: 健康检查负载
|
|
:param database_payload: 数据库状态负载
|
|
:param cache_payload: 缓存状态负载
|
|
:param deps_payload: 运维依赖负载
|
|
:param server_payload: 服务器摘要负载
|
|
:return: 原始面板列表
|
|
"""
|
|
panels = [
|
|
self.build_app_env_panel(env, app_env_payload, app_routes_payload),
|
|
self.build_health_panel(doctor_payload),
|
|
self.build_inspection_conclusion_panel(doctor_payload, database_payload, cache_payload),
|
|
self.build_recommended_entry_panel(doctor_payload, database_payload, cache_payload),
|
|
self.build_database_panel(database_payload),
|
|
self.build_cache_panel(cache_payload),
|
|
self.build_dependency_panel(deps_payload),
|
|
self.build_server_info_panel(server_payload),
|
|
]
|
|
panels.append(self.build_risk_heatmap_panel(panels))
|
|
return panels
|
|
|
|
|
|
class DashboardMetricBuilder:
|
|
"""
|
|
首页指标卡构建器。
|
|
|
|
该对象负责根据健康检查、数据库和缓存状态构建首页驾驶舱指标卡。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
"""
|
|
|
|
def __init__(self, formatting: DashboardFormattingSupport) -> None:
|
|
"""
|
|
初始化首页指标卡构建器。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
:return: None
|
|
"""
|
|
self.formatting = formatting
|
|
|
|
def build_metrics(
|
|
self,
|
|
doctor_payload: dict[str, Any] | None,
|
|
database_payload: dict[str, Any] | None,
|
|
cache_payload: dict[str, Any] | None,
|
|
) -> list[DashboardMetricSnapshot]:
|
|
"""
|
|
构建首页驾驶舱指标卡。
|
|
|
|
:param doctor_payload: 健康检查负载
|
|
:param database_payload: 数据库状态负载
|
|
:param cache_payload: 缓存状态负载
|
|
:return: 指标卡列表
|
|
"""
|
|
database = doctor_payload.get('database') if isinstance(doctor_payload, dict) else None
|
|
redis = doctor_payload.get('redis') if isinstance(doctor_payload, dict) else None
|
|
crypto = doctor_payload.get('crypto') if isinstance(doctor_payload, dict) else None
|
|
|
|
dependency_results = [
|
|
bool(database.get('ok', False)) if isinstance(database, dict) else False,
|
|
bool(redis.get('ok', False)) if isinstance(redis, dict) else False,
|
|
bool(crypto.get('ok', False)) if isinstance(crypto, dict) else False,
|
|
]
|
|
passed_dependencies = sum(1 for item in dependency_results if item)
|
|
posture = '风险' if isinstance(doctor_payload, dict) and not doctor_payload.get('ok', False) else '稳定'
|
|
posture_status = 'fail' if posture == '风险' else 'ok'
|
|
current_revision = database_payload.get('currentRevision', '-') if isinstance(database_payload, dict) else '-'
|
|
cache_size = cache_payload.get('dbSize', '-') if isinstance(cache_payload, dict) else '-'
|
|
|
|
metrics = [
|
|
DashboardMetricSnapshot(
|
|
title='当前态势',
|
|
value=f'{self.formatting.render_signal_bar(passed_dependencies, DEPENDENCY_CHECK_TOTAL)} {posture}',
|
|
status=posture_status,
|
|
hint=f'依赖通过 {passed_dependencies}/{DEPENDENCY_CHECK_TOTAL},健康检查自动判定',
|
|
),
|
|
DashboardMetricSnapshot(
|
|
title='依赖通过率',
|
|
value=(
|
|
f'{self.formatting.render_signal_bar(passed_dependencies, DEPENDENCY_CHECK_TOTAL)} '
|
|
f'{passed_dependencies}/{DEPENDENCY_CHECK_TOTAL}'
|
|
),
|
|
status=(
|
|
'ok'
|
|
if passed_dependencies == DEPENDENCY_CHECK_TOTAL
|
|
else 'warn'
|
|
if passed_dependencies > 0
|
|
else 'fail'
|
|
),
|
|
hint='数据库、Redis、加密组件',
|
|
),
|
|
DashboardMetricSnapshot(
|
|
title='迁移版本',
|
|
value=f'[{SHELL_TEXT_FORMATTER.truncate_text(current_revision, 16)}]',
|
|
status='ok' if current_revision != '-' else 'warn',
|
|
hint='当前数据库 revision 基线',
|
|
),
|
|
DashboardMetricSnapshot(
|
|
title='Redis 键数',
|
|
value=f'[KEYS] {cache_size}',
|
|
status='info',
|
|
hint='当前缓存库 key 数量观测值',
|
|
),
|
|
]
|
|
return sorted(
|
|
metrics,
|
|
key=lambda item: self.formatting.resolve_status_priority(item.status),
|
|
)
|
|
|
|
|
|
class DashboardAdapter:
|
|
"""
|
|
TUI 首页巡检聚合适配器。
|
|
|
|
该适配器负责采集应用、健康检查、数据库、缓存与运维只读快照,
|
|
并委托面板构建器、指标构建器和压缩器组装首页结果。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
formatting: DashboardFormattingSupport | None = None,
|
|
panel_builder: DashboardPanelBuilder | None = None,
|
|
metric_builder: DashboardMetricBuilder | None = None,
|
|
panel_compressor: DashboardPanelCompressor | None = None,
|
|
snapshot_collector: DashboardSnapshotCollector | None = None,
|
|
) -> None:
|
|
"""
|
|
初始化首页巡检聚合适配器。
|
|
|
|
:param formatting: 首页仪表盘格式化支持对象
|
|
:param panel_builder: 首页面板构建器
|
|
:param metric_builder: 首页指标卡构建器
|
|
:param panel_compressor: 首页面板压缩器
|
|
:param snapshot_collector: 首页聚合数据采集器
|
|
:return: None
|
|
"""
|
|
self.formatting = formatting or DashboardFormattingSupport()
|
|
self.panel_builder = panel_builder or DashboardPanelBuilder(self.formatting)
|
|
self.metric_builder = metric_builder or DashboardMetricBuilder(self.formatting)
|
|
self.panel_compressor = panel_compressor or DashboardPanelCompressor(self.formatting)
|
|
self.snapshot_collector = snapshot_collector or DashboardSnapshotCollector()
|
|
|
|
def collect_snapshot(self, env: str) -> DashboardSnapshot:
|
|
"""
|
|
采集 TUI 首页只读巡检快照。
|
|
|
|
:param env: 当前运行环境
|
|
:return: 首页聚合快照
|
|
"""
|
|
source_payloads = self.snapshot_collector.collect(env)
|
|
panels = self.panel_builder.build_panels(
|
|
env=env,
|
|
app_env_payload=source_payloads.app_env_payload,
|
|
app_routes_payload=source_payloads.app_routes_payload,
|
|
doctor_payload=source_payloads.doctor_payload,
|
|
database_payload=source_payloads.database_payload,
|
|
cache_payload=source_payloads.cache_payload,
|
|
deps_payload=source_payloads.deps_payload,
|
|
server_payload=source_payloads.server_payload,
|
|
)
|
|
return DashboardSnapshot(
|
|
env=env,
|
|
metrics=self.metric_builder.build_metrics(
|
|
source_payloads.doctor_payload,
|
|
source_payloads.database_payload,
|
|
source_payloads.cache_payload,
|
|
),
|
|
panels=self.panel_compressor.compact_panels(panels),
|
|
)
|
|
|
|
|
|
DASHBOARD_ADAPTER = DashboardAdapter()
|