mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-01 08:12:07 +00:00
* feat: 新增cli系统 * feat: ruoyi completion支持powershell * perf: 优化tui显示 * fix: 修复前端构建异常
582 lines
20 KiB
Python
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)
|