Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/cli/tui/diagnostics.py
T
insistence 9f7a0daa3b feat: 新增cli系统 (#103)
* feat: 新增cli系统

* feat: ruoyi completion支持powershell

* perf: 优化tui显示

* fix: 修复前端构建异常
2026-05-13 11:19:31 +08:00

582 lines
20 KiB
Python

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from cli.utils import SHELL_TEXT_FORMATTER
@dataclass(frozen=True)
class DiagnosticPagePolicy:
"""
TUI 页面诊断策略定义。
:param focus_terms: 页面聚焦词
:param subtitle_builder: 页面摘要构建函数
"""
focus_terms: tuple[str, ...]
subtitle_builder: Callable[..., str]
@dataclass(frozen=True)
class DiagnosticPagePolicyRegistry:
"""
TUI 页面诊断策略注册表。
:param policies: 页面诊断策略映射
"""
policies: dict[str, DiagnosticPagePolicy]
def get(self, page_key: str) -> DiagnosticPagePolicy:
"""
读取指定页面诊断策略。
:param page_key: 页面标识
:return: 页面诊断策略
:raises KeyError: 页面未注册时抛出异常
"""
return self.policies[page_key]
class TuiDiagnosticService:
"""
统一封装 TUI 页面诊断提示与摘要拼装逻辑。
该服务通过页面级诊断策略注册表管理各页面的聚焦词和 subtitle 规则,
供 adapter、dashboard 等对象复用。
:param page_policy_registry: 页面诊断策略注册表
"""
def __init__(self, page_policy_registry: DiagnosticPagePolicyRegistry) -> None:
"""
初始化诊断服务。
:param page_policy_registry: 页面诊断策略注册表
:return: None
"""
self.page_policy_registry = page_policy_registry
@staticmethod
def build_focus_hint(terms: tuple[str, ...]) -> str:
"""
将诊断聚焦词渲染为统一提示文本。
:param terms: 聚焦词元组
:return: 提示文本
"""
return f'聚焦:{" / ".join(terms)}'
def build_page_focus_hint(self, page_key: str) -> str:
"""
构建指定页面统一聚焦词。
:param page_key: 页面标识
:return: 聚焦提示
"""
return self.build_focus_hint(self.page_policy_registry.get(page_key).focus_terms)
def build_database_focus_hint(self) -> str:
"""
构建数据库页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('database')
def build_ops_focus_hint(self) -> str:
"""
构建运维页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('ops')
def build_cache_focus_hint(self) -> str:
"""
构建缓存页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('cache')
def build_app_focus_hint(self) -> str:
"""
构建应用页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('app')
def build_jobs_focus_hint(self) -> str:
"""
构建任务页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('jobs')
def build_configs_focus_hint(self) -> str:
"""
构建参数配置页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('configs')
def build_gen_focus_hint(self) -> str:
"""
构建代码生成页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('gen')
def build_crypto_focus_hint(self) -> str:
"""
构建加密页统一聚焦词。
:return: 聚焦提示
"""
return self.build_page_focus_hint('crypto')
@staticmethod
def _build_app_subtitle(
env_payload: dict[str, Any] | None,
config_payload: dict[str, Any] | None,
doctor_payload: dict[str, Any] | None,
routes_payload: dict[str, Any] | None,
completion_payload: dict[str, Any] | None,
*,
focus_hint: str,
) -> str:
"""
构建应用页诊断摘要。
:param env_payload: `app env` 结果
:param config_payload: `app config` 结果
:param doctor_payload: `app doctor` 结果
:param routes_payload: `app routes` 结果
:param completion_payload: `completion doctor` 结果
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
env_ok = bool(isinstance(env_payload, dict) and env_payload.get('ok', False))
config_ok = bool(isinstance(config_payload, dict) and config_payload.get('ok', False))
doctor_ok = bool(isinstance(doctor_payload, dict) and doctor_payload.get('ok', False))
routes_ok = bool(isinstance(routes_payload, dict) and routes_payload.get('ok', False))
completion_ok = bool(isinstance(completion_payload, dict) and completion_payload.get('ok', False))
route_count = routes_payload.get('count', 0) if isinstance(routes_payload, dict) else 0
if not env_ok or not config_ok:
return f'{focus_hint} | 应用基础信息读取异常,优先确认环境映射与配置摘要'
if not doctor_ok:
return f'{focus_hint} | 启动前检查异常,优先确认数据库、Redis 与加密组件状态'
if not completion_ok:
return f'{focus_hint} | 补全诊断读取异常,优先确认项目目录与 shell 补全配置'
if not routes_ok:
return f'{focus_hint} | 路由摘要读取异常,优先确认应用初始化与路由注册状态'
return (
f'{focus_hint} | 当前已加载 {route_count} 条注册路由,'
'可继续查看环境映射、配置摘要、启动前检查、补全诊断与路由状态'
)
@staticmethod
def _build_database_subtitle(
revision_payload: dict[str, Any] | None,
check_payload: dict[str, Any] | None,
heads_payload: dict[str, Any] | None,
*,
focus_hint: str,
) -> str:
"""
构建数据库页诊断摘要。
:param revision_payload: `db current` 结果
:param check_payload: `db check` 结果
:param heads_payload: `db heads` 结果
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
revision = revision_payload.get('currentRevision', '-') if isinstance(revision_payload, dict) else '-'
revision_text = SHELL_TEXT_FORMATTER.truncate_text(str(revision or '-'), 48)
revision_ok = bool(isinstance(revision_payload, dict) and revision_payload.get('ok', False))
check_ok = bool(isinstance(check_payload, dict) and check_payload.get('ok', False))
heads_ok = bool(isinstance(heads_payload, dict) and heads_payload.get('ok', False))
heads_items = heads_payload.get('items') if isinstance(heads_payload, dict) else None
heads_count = len(heads_items) if isinstance(heads_items, list) else 0
if not revision_ok or not check_ok:
return f'{focus_hint} | 数据库异常,优先确认迁移版本、连接状态与 heads 信息'
if not heads_ok or heads_count != 1:
return f'{focus_hint} | 数据库存在迁移分叉风险,优先确认 heads 和历史版本'
return f'{focus_hint} | 数据库基线正常,当前 revision {revision_text},可继续查看连接、heads 和历史版本'
@staticmethod
def _build_ops_subtitle(
health_payload: dict[str, Any] | None,
deps_payload: dict[str, Any] | None,
server_payload: dict[str, Any] | None,
*,
focus_hint: str,
) -> str:
"""
构建运维页诊断摘要。
:param health_payload: `ops health` 结果
:param deps_payload: `ops deps` 结果
:param server_payload: `ops server-info` 结果
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
health_ok = bool(isinstance(health_payload, dict) and health_payload.get('ok', False))
deps_ok = bool(isinstance(deps_payload, dict) and deps_payload.get('ok', False))
server_ok = bool(isinstance(server_payload, dict) and server_payload.get('ok', False))
if not health_ok:
return f'{focus_hint} | 运维探活存在异常,优先核对数据库/Redis 连通性与依赖版本'
if not deps_ok:
return f'{focus_hint} | 运维依赖存在异常,优先核对缺失依赖与版本兼容性'
if not server_ok:
return f'{focus_hint} | 服务器信息采集异常,优先重新检查主机资源与运行环境'
return f'{focus_hint} | 基础探活和依赖状态正常,可继续查看服务器资源与磁盘样本'
@staticmethod
def _build_cache_subtitle(
stats_payload: dict[str, Any] | None,
matched_count: int,
*,
focus_hint: str,
) -> str:
"""
构建缓存页诊断摘要。
:param stats_payload: `cache stats` 结果
:param matched_count: 当前筛选后缓存名数量
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
db_size = stats_payload.get('dbSize', '-') if isinstance(stats_payload, dict) else '-'
client_count = '-'
if isinstance(stats_payload, dict) and isinstance(stats_payload.get('info'), dict):
client_count = stats_payload['info'].get('connected_clients', '-') # type: ignore[index]
return (
f'{focus_hint} | 缓存基线正常,当前已加载 {matched_count} 个缓存名,'
f'Redis 键数 {db_size},连接数 {client_count},可继续查看键列表、键值样本和 TTL'
)
@staticmethod
def _build_jobs_subtitle(
filter_label: str,
matched_count: int,
failed_job_names: set[str],
paused_count: int,
*,
focus_hint: str,
) -> str:
"""
构建任务页诊断摘要。
:param filter_label: 当前筛选标签
:param matched_count: 当前匹配任务数
:param failed_job_names: 最近失败任务集合
:param paused_count: 暂停任务数量
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
if failed_job_names:
return (
f'{focus_hint} | 当前筛选:{filter_label},已匹配 {matched_count} 条任务,'
f'失败任务 {len(failed_job_names)} 个,暂停任务 {paused_count} 个'
)
return (
f'{focus_hint} | 当前筛选:{filter_label},已匹配 {matched_count} 条任务,'
f'当前没有失败任务,暂停任务 {paused_count} 个'
)
@staticmethod
def _build_configs_subtitle(
filter_label: str,
matched_count: int,
mismatch_count: int,
drift_count: int,
*,
focus_hint: str,
) -> str:
"""
构建参数配置页诊断摘要。
:param filter_label: 当前筛选标签
:param matched_count: 当前匹配配置数
:param mismatch_count: 值不一致数量
:param drift_count: 缓存漂移数量
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
return (
f'{focus_hint} | 当前筛选:{filter_label},已匹配 {matched_count} 项配置,'
f'值不一致 {mismatch_count} 项,缓存漂移 {drift_count} 项'
)
@staticmethod
def _build_gen_subtitle(
matched_count: int,
importable_count: int,
*,
focus_hint: str,
) -> str:
"""
构建代码生成页诊断摘要。
:param matched_count: 当前匹配业务表数
:param importable_count: 可导入物理表数
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
return (
f'{focus_hint} | 当前已匹配 {matched_count} 张业务表,'
f'可导入物理表 {importable_count} 张,可继续查看表定义、预检查和代码预览'
)
@staticmethod
def _build_crypto_subtitle(
validate_payload: dict[str, Any] | None,
public_payload: dict[str, Any] | None,
*,
focus_hint: str,
) -> str:
"""
构建加密页诊断摘要。
:param validate_payload: `crypto validate` 结果
:param public_payload: `crypto export-public` 结果
:param focus_hint: 聚焦提示
:return: 页面摘要
"""
validate_ok = bool(isinstance(validate_payload, dict) and validate_payload.get('ok', False))
public_ok = bool(isinstance(public_payload, dict) and public_payload.get('ok', False))
public_key_payload = public_payload.get('publicKey') if isinstance(public_payload, dict) else None
current_kid = public_key_payload.get('kid', '-') if isinstance(public_key_payload, dict) else '-'
supported_kids = public_key_payload.get('supportedKids') if isinstance(public_key_payload, dict) else None
supported_count = len(supported_kids) if isinstance(supported_kids, list) else 0
if not validate_ok:
return f'{focus_hint} | 运行校验失败,优先确认传输加密配置与环境变量'
if not public_ok or not isinstance(public_key_payload, dict):
return f'{focus_hint} | 公钥导出结果异常,优先确认当前 KID 与公钥内容'
return (
f'{focus_hint} | 当前 KID {current_kid},兼容版本 {supported_count} 个,'
'可继续查看公钥身份、兼容版本与轮换预演入口'
)
def build_app_diagnostic_subtitle(
self,
env_payload: dict[str, Any] | None,
config_payload: dict[str, Any] | None,
doctor_payload: dict[str, Any] | None,
routes_payload: dict[str, Any] | None,
completion_payload: dict[str, Any] | None,
) -> str:
"""
构建应用页统一诊断摘要。
:param env_payload: `app env` 结果
:param config_payload: `app config` 结果
:param doctor_payload: `app doctor` 结果
:param routes_payload: `app routes` 结果
:param completion_payload: `completion doctor` 结果
:return: 页面摘要
"""
return self.page_policy_registry.get('app').subtitle_builder(
env_payload,
config_payload,
doctor_payload,
routes_payload,
completion_payload,
focus_hint=self.build_app_focus_hint(),
)
def build_database_diagnostic_subtitle(
self,
revision_payload: dict[str, Any] | None,
check_payload: dict[str, Any] | None,
heads_payload: dict[str, Any] | None,
) -> str:
"""
构建数据库页统一诊断摘要。
:param revision_payload: `db current` 结果
:param check_payload: `db check` 结果
:param heads_payload: `db heads` 结果
:return: 页面摘要
"""
return self.page_policy_registry.get('database').subtitle_builder(
revision_payload,
check_payload,
heads_payload,
focus_hint=self.build_database_focus_hint(),
)
def build_ops_diagnostic_subtitle(
self,
health_payload: dict[str, Any] | None,
deps_payload: dict[str, Any] | None,
server_payload: dict[str, Any] | None,
) -> str:
"""
构建运维页统一诊断摘要。
:param health_payload: `ops health` 结果
:param deps_payload: `ops deps` 结果
:param server_payload: `ops server-info` 结果
:return: 页面摘要
"""
return self.page_policy_registry.get('ops').subtitle_builder(
health_payload,
deps_payload,
server_payload,
focus_hint=self.build_ops_focus_hint(),
)
def build_cache_diagnostic_subtitle(
self,
stats_payload: dict[str, Any] | None,
matched_count: int,
) -> str:
"""
构建缓存页统一诊断摘要。
:param stats_payload: `cache stats` 结果
:param matched_count: 当前筛选后缓存名数量
:return: 页面摘要
"""
return self.page_policy_registry.get('cache').subtitle_builder(
stats_payload,
matched_count,
focus_hint=self.build_cache_focus_hint(),
)
def build_jobs_diagnostic_subtitle(
self,
filter_label: str,
matched_count: int,
failed_job_names: set[str],
paused_count: int,
) -> str:
"""
构建任务页统一诊断摘要。
:param filter_label: 当前筛选标签
:param matched_count: 当前匹配任务数
:param failed_job_names: 最近失败任务集合
:param paused_count: 暂停任务数量
:return: 页面摘要
"""
return self.page_policy_registry.get('jobs').subtitle_builder(
filter_label,
matched_count,
failed_job_names,
paused_count,
focus_hint=self.build_jobs_focus_hint(),
)
def build_configs_diagnostic_subtitle(
self,
filter_label: str,
matched_count: int,
mismatch_count: int,
drift_count: int,
) -> str:
"""
构建参数配置页统一诊断摘要。
:param filter_label: 当前筛选标签
:param matched_count: 当前匹配配置数
:param mismatch_count: 值不一致数量
:param drift_count: 缓存漂移数量
:return: 页面摘要
"""
return self.page_policy_registry.get('configs').subtitle_builder(
filter_label,
matched_count,
mismatch_count,
drift_count,
focus_hint=self.build_configs_focus_hint(),
)
def build_gen_diagnostic_subtitle(
self,
matched_count: int,
importable_count: int,
) -> str:
"""
构建代码生成页统一诊断摘要。
:param matched_count: 当前匹配业务表数
:param importable_count: 可导入物理表数
:return: 页面摘要
"""
return self.page_policy_registry.get('gen').subtitle_builder(
matched_count,
importable_count,
focus_hint=self.build_gen_focus_hint(),
)
def build_crypto_diagnostic_subtitle(
self,
validate_payload: dict[str, Any] | None,
public_payload: dict[str, Any] | None,
) -> str:
"""
构建加密页统一诊断摘要。
:param validate_payload: `crypto validate` 结果
:param public_payload: `crypto export-public` 结果
:return: 页面摘要
"""
return self.page_policy_registry.get('crypto').subtitle_builder(
validate_payload,
public_payload,
focus_hint=self.build_crypto_focus_hint(),
)
TUI_DIAGNOSTIC_PAGE_POLICY_REGISTRY = DiagnosticPagePolicyRegistry(
policies={
'database': DiagnosticPagePolicy(
focus_terms=('迁移版本', '连接状态', 'Heads'),
subtitle_builder=TuiDiagnosticService._build_database_subtitle,
),
'ops': DiagnosticPagePolicy(
focus_terms=('数据库连通性', 'Redis 连通性', '依赖版本'),
subtitle_builder=TuiDiagnosticService._build_ops_subtitle,
),
'cache': DiagnosticPagePolicy(
focus_terms=('Redis 键数', '连接数', '缓存名前缀'),
subtitle_builder=TuiDiagnosticService._build_cache_subtitle,
),
'app': DiagnosticPagePolicy(
focus_terms=('环境映射', '配置摘要', '启动前检查'),
subtitle_builder=TuiDiagnosticService._build_app_subtitle,
),
'jobs': DiagnosticPagePolicy(
focus_terms=('失败聚合', '暂停任务', '执行轨迹'),
subtitle_builder=TuiDiagnosticService._build_jobs_subtitle,
),
'configs': DiagnosticPagePolicy(
focus_terms=('高风险配置', '值不一致', '缓存漂移'),
subtitle_builder=TuiDiagnosticService._build_configs_subtitle,
),
'gen': DiagnosticPagePolicy(
focus_terms=('生成前校验', '同步预检查', '代码预览'),
subtitle_builder=TuiDiagnosticService._build_gen_subtitle,
),
'crypto': DiagnosticPagePolicy(
focus_terms=('运行校验', '公钥身份', '兼容版本'),
subtitle_builder=TuiDiagnosticService._build_crypto_subtitle,
),
}
)
TUI_DIAGNOSTIC_SERVICE = TuiDiagnosticService(TUI_DIAGNOSTIC_PAGE_POLICY_REGISTRY)