ShiErFeng/shierfeng-fastapi-backend/cli/tui/diagnostics.py
2026-07-24 09:43:10 +08:00

582 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)