from dataclasses import dataclass from typing import Any from cli.tui.adapters.base import BaseDetailAdapter from cli.tui.adapters.models import ( TUI_ADAPTER_MODEL_RENDERER, DetailPageSnapshot, DetailSectionSnapshot, ) from cli.tui.copy import TUI_COPY from cli.tui.diagnostics import TUI_DIAGNOSTIC_SERVICE from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER @dataclass(frozen=True) class CryptoDetailSourcePayloads: """ 传输加密详情页原始数据源快照。 :param validate_payload: `crypto validate` 结果 :param public_payload: `crypto export-public` 结果 """ validate_payload: dict[str, Any] | None public_payload: dict[str, Any] | None class CryptoDetailSnapshotCollector: """ 传输加密详情页数据采集器。 该对象负责拉取传输加密详情页所需的 CLI 原始结果, 让 `CryptoDetailAdapter` 保持详情页编排职责。 """ def collect(self, env: str) -> CryptoDetailSourcePayloads: """ 采集传输加密详情页所需原始结果。 :param env: 当前运行环境 :return: 传输加密详情页原始数据源快照 """ return CryptoDetailSourcePayloads( validate_payload=NESTED_CLI_SUPPORT.run( 'crypto', 'validate', f'--env={env}', '--output=json', parse_json=True, ).payload, public_payload=NESTED_CLI_SUPPORT.run( 'crypto', 'export-public', f'--env={env}', '--output=json', parse_json=True, ).payload, ) class CryptoSectionBuilder: """ 传输加密详情分区构建器。 该构建器负责将传输加密相关 CLI 结果负载转换为 TUI 详情页分区, 使详情页适配器本体只保留采集与编排职责。 """ @staticmethod def build_validate_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: """ 构建传输加密运行校验分区。 :param payload: `crypto validate` JSON 负载 :return: 分区快照 """ if not isinstance(payload, dict): return DetailSectionSnapshot( title='运行校验', status='fail', lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( payload, empty_label='运行校验', empty_value='不可用' ), ) lines = [ '## 校验结果', f'状态: {"通过" if payload.get("ok", False) else "失败"}', f'说明: {SHELL_TEXT_FORMATTER.truncate_text(TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(payload), 72)}', ] if payload.get('error'): lines.append(f'错误: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("error", "-"), 72)}') return DetailSectionSnapshot( title='运行校验', status='ok' if payload.get('ok', False) else 'fail', lines=lines, ) @staticmethod def build_public_identity_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: """ 构建公钥身份分区。 :param payload: `crypto export-public` JSON 负载 :return: 分区快照 """ public_key_payload = payload.get('publicKey') if isinstance(payload, dict) else None if not isinstance(public_key_payload, dict): return DetailSectionSnapshot( title='公钥身份', status='fail', lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='公钥', empty_value='不可用'), ) return DetailSectionSnapshot( title='公钥身份', status='ok' if payload.get('ok', False) else 'fail', lines=[ '## 当前版本', f'KID: {public_key_payload.get("kid", "-")}', f'算法: {public_key_payload.get("alg", "-")}', f'信封版本: {public_key_payload.get("envelopeVersion", "-")}', f'过期时间: {public_key_payload.get("expireAt", "-")}', ], ) @staticmethod def build_supported_kids_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: """ 构建兼容版本分区。 :param payload: `crypto export-public` JSON 负载 :return: 分区快照 """ public_key_payload = payload.get('publicKey') if isinstance(payload, dict) else None if not isinstance(public_key_payload, dict): return DetailSectionSnapshot( title='兼容版本', status='fail', lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( payload, empty_label='兼容版本', empty_value='不可用' ), ) supported_kids = public_key_payload.get('supportedKids') lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( empty_label='兼容版本', empty_value='0 个', detail='当前公钥未声明兼容版本 KID', suggestion='如需兼容旧版本客户端,可检查 supportedKids 配置', ) if isinstance(supported_kids, list) and supported_kids: lines = ['## 兼容 KID', *[f'支持版本: {kid}' for kid in supported_kids[:10]]] return DetailSectionSnapshot( title='兼容版本', status='ok', lines=lines, ) def build_public_preview_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: """ 构建公钥预览分区。 :param payload: `crypto export-public` JSON 负载 :return: 分区快照 """ public_key_payload = payload.get('publicKey') if isinstance(payload, dict) else None if not isinstance(public_key_payload, dict): return DetailSectionSnapshot( title='公钥预览', status='fail', lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( payload, empty_label='公钥预览', empty_value='不可用' ), ) public_key = str(public_key_payload.get('publicKey', '') or '').splitlines() if not public_key: return DetailSectionSnapshot( title='公钥预览', status='info', lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( empty_label='公钥预览', empty_value='无内容', detail='当前公钥内容为空,无法生成预览', suggestion='可检查公钥导出结果,或重新生成公钥后再刷新', ), ) return DetailSectionSnapshot( title='公钥预览', status='info', lines=['## 预览内容', *[SHELL_TEXT_FORMATTER.truncate_text(line, 88) for line in public_key[:6]]], ) @staticmethod def build_overview_section( validate_payload: dict[str, Any] | None, public_payload: dict[str, Any] | None, ) -> DetailSectionSnapshot: """ 构建加密页总览判断分区。 :param validate_payload: `crypto validate` JSON 负载 :param public_payload: `crypto export-public` JSON 负载 :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 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 status = 'ok' conclusion = '传输加密基线正常,可继续查看公钥身份、兼容版本与预演入口' if not validate_ok: status = 'fail' conclusion = '运行校验失败,优先确认传输加密配置与环境变量' elif not public_ok or not isinstance(public_key_payload, dict): status = 'warn' conclusion = '公钥导出结果异常,建议先确认当前 KID 与公钥内容是否可用' return DetailSectionSnapshot( title='总览判断', status=status, lines=[ '## 当前结论', conclusion, '', '## 核心指标', f'运行校验: {"通过" if validate_ok else "失败"}', f'当前 KID: {public_key_payload.get("kid", "-") if isinstance(public_key_payload, dict) else "-"}', f'算法: {public_key_payload.get("alg", "-") if isinstance(public_key_payload, dict) else "-"}', f'兼容版本数: {supported_count}', '', '## 建议入口', '优先关注:运行校验 / 公钥身份 / 兼容版本 / 轮换预演入口', ], ) @staticmethod def build_rotation_entry_section(env: str) -> DetailSectionSnapshot: """ 构建加密密钥轮换预演入口分区。 :param env: 当前运行环境 :return: 分区快照 """ return DetailSectionSnapshot( title='轮换预演入口', status='warn', lines=TUI_COPY.build_command_hint_lines( scenario='准备轮换传输加密密钥时,应先做 dry-run,确认新 KID、密钥长度和 guard 提示后再执行正式轮换。', command=TUI_COPY.build_cli_command_hint( 'crypto', 'rotate', f'--env={env}', '--dry-run', '--output=json', ), guide='当前页面先提供预演入口;正式轮换仍应遵循危险命令确认流程,避免直接在未知环境落地变更。', ), ) @staticmethod def build_keygen_entry_section(env: str) -> DetailSectionSnapshot: """ 构建加密密钥生成入口分区。 :param env: 当前运行环境 :return: 分区快照 """ return DetailSectionSnapshot( title='密钥生成入口', status='warn', lines=TUI_COPY.build_command_hint_lines( scenario='准备生成新的传输加密密钥对时,应在当前终端直接查看生成结果,并立即核对 KID、密钥长度和环境变量补丁。', command=TUI_COPY.build_cli_command_hint( 'crypto', 'keygen', f'--env={env}', '--output=text', ), guide='当前入口会在终端中直接输出新公钥、私钥和 env patch keys,后续再决定是否进入轮换流程。', ), ) def build_sections( self, *, validate_payload: dict[str, Any] | None, public_payload: dict[str, Any] | None, env: str, ) -> list[DetailSectionSnapshot]: """ 构建传输加密页全部详情分区。 :param validate_payload: `crypto validate` 结果负载 :param public_payload: `crypto export-public` 结果负载 :param env: 当前运行环境 :return: 详情分区列表 """ return [ self.build_overview_section(validate_payload, public_payload), self.build_validate_section(validate_payload), self.build_public_identity_section(public_payload), self.build_supported_kids_section(public_payload), self.build_public_preview_section(public_payload), self.build_keygen_entry_section(env), self.build_rotation_entry_section(env), ] class CryptoDetailAdapter(BaseDetailAdapter): """ 传输加密详情页适配器。 该适配器负责采集传输加密相关 CLI 结果,并组装为 TUI 详情页快照。 页面私有的解析逻辑、分区构建逻辑和快照采集流程统一收口在该类中。 :param section_builder: 传输加密详情分区构建器 """ def __init__( self, section_builder: CryptoSectionBuilder | None = None, snapshot_collector: CryptoDetailSnapshotCollector | None = None, ) -> None: """ 初始化传输加密详情页适配器。 :param section_builder: 传输加密详情分区构建器 :param snapshot_collector: 传输加密详情页数据采集器 :return: None """ super().__init__( page_title='传输加密', search_view_key='crypto', default_suggestions=[ '总览判断', '运行校验', '公钥身份', '兼容版本', '公钥预览', '密钥生成入口', '轮换预演入口', ], ) self.section_builder = section_builder or CryptoSectionBuilder() self.snapshot_collector = snapshot_collector or CryptoDetailSnapshotCollector() def collect_snapshot(self, env: str, query: str = '') -> DetailPageSnapshot: """ 采集传输加密状态页只读快照。 :param env: 当前运行环境 :param query: 当前搜索词 :return: 页面快照 """ source_payloads = self.snapshot_collector.collect(env) sections = self.section_builder.build_sections( validate_payload=source_payloads.validate_payload, public_payload=source_payloads.public_payload, env=env, ) return DetailPageSnapshot( title='传输加密', subtitle=TUI_DIAGNOSTIC_SERVICE.build_crypto_diagnostic_subtitle( source_payloads.validate_payload, source_payloads.public_payload, ), sections=self.filter_sections(sections, query), search=self.resolve_search_context(query), ) CRYPTO_DETAIL_ADAPTER = CryptoDetailAdapter()