diff --git a/.gitignore b/.gitignore index df81628..0d713af 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,8 @@ __pycache__/ node_modules/ /.venv/ /venv/ +/shierfeng/ + +# Python 安装与构建产物,源码以 cli/ 为准。 +*.egg-info/ +/shierfeng-fastapi-backend/build/ diff --git a/shierfeng-fastapi-backend/build/lib/cli/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/bootstrap.py b/shierfeng-fastapi-backend/build/lib/cli/bootstrap.py deleted file mode 100644 index b70f42f..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/bootstrap.py +++ /dev/null @@ -1,56 +0,0 @@ -import os - -from cli.runtime import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService - - -class AppBootstrapService: - """ - 应用引导服务。 - - 该服务负责解析应用原始入口路径、构建启动命令,并以 `exec` - 方式将当前 CLI 进程切换为 FastAPI 应用入口进程。 - """ - - def __init__(self, *, runtime_environment: RuntimeEnvironmentService | None = None) -> None: - """ - 初始化应用引导服务。 - - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT - - def get_app_entry_path(self) -> str: - """ - 获取应用启动入口路径。 - - :return: 应用启动入口绝对路径 - """ - return os.path.join(self.runtime_environment.get_backend_dir(), 'app.py') - - def build_app_run_command(self, env: str) -> list[str]: - """ - 构建应用启动命令。 - - :param env: 运行环境 - :return: 应用启动命令参数列表 - """ - return [ - self.runtime_environment.get_python_executable(), - self.get_app_entry_path(), - '--env', - env, - ] - - def exec_app_run_command(self, env: str) -> None: - """ - 以应用原始入口进程启动当前 FastAPI 应用。 - - :param env: 运行环境 - :return: None - """ - command = self.build_app_run_command(env) - os.execvp(command[0], command) - - -APP_BOOTSTRAP = AppBootstrapService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/completion/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/commands.py b/shierfeng-fastapi-backend/build/lib/cli/completion/commands.py deleted file mode 100644 index 1dce26b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/commands.py +++ /dev/null @@ -1,155 +0,0 @@ -from dataclasses import dataclass, field -from pathlib import Path - -import typer - -from cli.completion.controller import CompletionCommandController -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway -from cli.context import OutputOption - - -@dataclass(frozen=True) -class CompletionCommandRegistration: - """ - completion 子命令注册描述。 - - :param name: 子命令名称 - :param help_text: 子命令帮助文本 - """ - - name: str - help_text: str - - -@dataclass -class CompletionSubcommandRegistrar: - """ - completion 子命令注册器。 - - 该对象负责将 `show/install/doctor` 子命令挂载到 completion 应用, - 将 Typer 命令声明细节从构建器主体中拆出。 - - :param completion_provider_gateway: completion 提供器对外网关 - """ - - completion_provider_gateway: CompletionProviderGateway = field(default_factory=lambda: COMPLETION_PROVIDER_GATEWAY) - - @staticmethod - def build_registrations() -> tuple[CompletionCommandRegistration, ...]: - """ - 返回 completion 子命令注册描述列表。 - - :return: 子命令注册描述元组 - """ - return ( - CompletionCommandRegistration(name='show', help_text='输出指定 shell 的 completion 脚本'), - CompletionCommandRegistration(name='install', help_text='安装指定 shell 的 completion 脚本'), - CompletionCommandRegistration(name='doctor', help_text='检查当前 completion 配置状态'), - ) - - def register(self, app: typer.Typer, *, controller: CompletionCommandController) -> None: - """ - 向 completion 应用注册全部子命令。 - - :param app: completion 子应用 - :param controller: completion 命令控制器 - :return: None - """ - registrations = {registration.name: registration for registration in self.build_registrations()} - - @app.command(registrations['show'].name, help=registrations['show'].help_text) - def show( - shell: str = typer.Argument( - ..., - help='shell 名称,如 bash、zsh、fish', - autocompletion=self.completion_provider_gateway.complete_shell_names, - ), - ) -> None: - """ - 输出指定 shell 的 completion 脚本。 - - :param shell: shell 名称 - :return: None - """ - controller.show(shell) - - @app.command(registrations['install'].name, help=registrations['install'].help_text) - def install( - shell: str | None = typer.Option( - None, - '--shell', - help='shell 名称,默认自动识别当前 shell', - autocompletion=self.completion_provider_gateway.complete_shell_names, - ), - output: OutputOption = 'text', - target_file: Path | None = typer.Option(None, '--target-file', help='completion 脚本目标文件路径'), - activate: bool = typer.Option(False, '--activate', help='将激活命令写入 shell rc 文件'), - rc_file: Path | None = typer.Option(None, '--rc-file', help='自定义 shell rc 文件路径'), - force: bool = typer.Option(False, '--force', help='覆盖已存在且内容不同的目标文件'), - ) -> None: - """ - 安装指定 shell 的 completion 脚本。 - - :param shell: shell 名称,默认自动识别当前 shell - :param output: 输出格式 - :param target_file: completion 脚本目标文件路径 - :param activate: 是否写入 shell rc 文件激活命令 - :param rc_file: 自定义 shell rc 文件路径 - :param force: 是否强制覆盖已存在文件 - :return: None - """ - controller.install( - output, - shell=shell, - target_file=target_file, - activate=activate, - rc_file=rc_file, - force=force, - ) - - @app.command(registrations['doctor'].name, help=registrations['doctor'].help_text) - def doctor( - output: OutputOption = 'text', - ) -> None: - """ - 检查当前 completion 配置状态。 - - :param output: 输出格式 - :return: None - """ - controller.doctor(output) - - -@dataclass -class CompletionCommandBuilder: - """ - completion 子应用构建器。 - - 该构建器负责装配 `completion` 子应用,并将控制器实例和子命令注册 - 细节收口到类式对象协作中。 - - :param completion_subcommand_registrar: completion 子命令注册器 - """ - - completion_subcommand_registrar: CompletionSubcommandRegistrar = field( - default_factory=CompletionSubcommandRegistrar - ) - - def build(self, root_cli: typer.Typer) -> typer.Typer: - """ - 构建 completion 命令组。 - - :param root_cli: 根 Typer 应用 - :return: completion 子应用 - """ - app = typer.Typer( - help='shell completion 相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, - ) - completion_command_controller = CompletionCommandController(root_cli) - self.completion_subcommand_registrar.register(app, controller=completion_command_controller) - return app - - -COMPLETION_COMMAND_BUILDER = CompletionCommandBuilder() diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/controller.py b/shierfeng-fastapi-backend/build/lib/cli/completion/controller.py deleted file mode 100644 index 16a7632..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/controller.py +++ /dev/null @@ -1,116 +0,0 @@ -from pathlib import Path - -import typer - -from cli.completion.doctor import COMPLETION_DOCTOR, CompletionDoctorService -from cli.completion.installers import COMPLETION_INSTALLER, CompletionInstallerService -from cli.completion.presenter import CompletionCommandPresenter -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) - - -class CompletionCommandController: - """ - completion 命令控制器。 - - 该控制器负责组织 `completion` 命令组的上下文准备、payload 生成、 - 文本渲染与脚本输出收口。 - - :param root_cli: 根 Typer 应用 - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: completion 命令文本渲染器 - :param installer_service: completion 安装与脚本生成服务 - :param doctor_service: completion 诊断服务 - """ - - def __init__( - self, - root_cli: typer.Typer, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: CompletionCommandPresenter | None = None, - installer_service: CompletionInstallerService | None = None, - doctor_service: CompletionDoctorService | None = None, - ) -> None: - """ - 初始化 completion 命令控制器。 - - :param root_cli: 根 Typer 应用 - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: completion 命令文本渲染器 - :param installer_service: completion 安装与脚本生成服务 - :param doctor_service: completion 诊断服务 - :return: None - """ - self.root_cli = root_cli - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or CompletionCommandPresenter() - self.installer_service = installer_service or COMPLETION_INSTALLER - self.doctor_service = doctor_service or COMPLETION_DOCTOR - - def show(self, shell: str) -> None: - """ - 输出指定 shell 的 completion 脚本。 - - :param shell: shell 名称 - :return: None - """ - typer.echo(self.installer_service.render_completion_script(self.root_cli, shell), nl=False) - - def install( - self, - output: str, - *, - shell: str | None, - target_file: Path | None, - activate: bool, - rc_file: Path | None, - force: bool, - ) -> None: - """ - 安装指定 shell 的 completion 脚本。 - - :param output: 输出格式 - :param shell: shell 名称 - :param target_file: completion 脚本目标文件路径 - :param activate: 是否写入 shell rc 文件激活命令 - :param rc_file: 自定义 shell rc 文件路径 - :param force: 是否强制覆盖已存在文件 - :return: None - """ - ctx = self.context_factory.build_readonly('dev', output) - payload = self.installer_service.install_completion_script( - self.root_cli, - shell, - target_file=target_file, - activate=activate, - rc_file=rc_file, - force=force, - ) - self.execution_service.complete_payload_result( - ctx, - payload, - text_builder=self.presenter.build_completion_install_text, - ) - - def doctor(self, output: str) -> None: - """ - 检查当前 completion 配置状态。 - - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly('dev', output) - payload = self.doctor_service.build_completion_doctor_payload() - self.execution_service.complete_payload_result( - ctx, - payload, - text_builder=self.presenter.build_completion_doctor_text, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/doctor.py b/shierfeng-fastapi-backend/build/lib/cli/completion/doctor.py deleted file mode 100644 index 7d18d29..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/doctor.py +++ /dev/null @@ -1,95 +0,0 @@ -from pathlib import Path -from typing import Any - -from cli.completion.installers import ( - CLICK_COMPLETE_ENV_VAR, - COMPLETION_INSTALLER, - CompletionInstallerService, -) -from cli.metadata import ( - COMPLETION_SHELL_SPEC_REGISTRY, - ENVIRONMENT_OPTION_SERVICE, - CompletionShellSpecRegistry, - EnvironmentOptionService, -) - - -class CompletionDoctorService: - """ - completion 诊断服务。 - - 该服务负责汇总活跃 shell、目标脚本路径、rc 文件状态、推荐安装命令 - 与环境候选列表,生成 `completion doctor` 所需的结构化结果。 - - :param installer_service: completion 安装服务 - :param shell_spec_registry: shell 元数据注册表 - :param environment_option_service: 环境选项服务 - """ - - def __init__( - self, - *, - installer_service: CompletionInstallerService | None = None, - shell_spec_registry: CompletionShellSpecRegistry | None = None, - environment_option_service: EnvironmentOptionService | None = None, - ) -> None: - """ - 初始化 completion 诊断服务。 - - :param installer_service: completion 安装服务 - :param shell_spec_registry: shell 元数据注册表 - :param environment_option_service: 环境选项服务 - :return: None - """ - self.installer_service = installer_service or COMPLETION_INSTALLER - self.shell_spec_registry = shell_spec_registry or COMPLETION_SHELL_SPEC_REGISTRY - self.environment_option_service = environment_option_service or ENVIRONMENT_OPTION_SERVICE - - def build_completion_doctor_payload(self) -> dict[str, Any]: - """ - 构建 completion 诊断结果。 - - :return: 诊断结果字典 - """ - active_shell = self.installer_service.detect_active_shell() - shells: dict[str, dict[str, Any]] = {} - for shell_name, shell_spec in self.shell_spec_registry.specs.items(): - target_file = self.installer_service.resolve_completion_target(shell_name) - rc_file = self.installer_service.resolve_completion_rc_file(shell_name) - source_command = None - if shell_spec.supported and shell_spec.generator in {'click', 'custom'}: - source_command = self.installer_service.build_source_command(target_file, shell_name) - recommended_install_command = f'shierfeng completion install --shell={shell_name}' - if shell_spec.supported and not shell_spec.auto_discovery: - recommended_install_command = f'{recommended_install_command} --activate' - shells[shell_name] = { - 'supported': shell_spec.supported, - 'detected': shell_name == active_shell, - 'description': shell_spec.description, - 'targetFile': str(target_file), - 'targetFileExists': target_file.exists(), - 'rcFile': str(rc_file) if rc_file is not None else None, - 'rcFileExists': rc_file.exists() if rc_file is not None else None, - 'autoDiscovery': shell_spec.auto_discovery, - 'sourceCommand': source_command, - 'recommendedInstallCommand': recommended_install_command, - } - - recommended_shell = active_shell if self.shell_spec_registry.get_spec(active_shell) is not None else None - recommended_install_command = None - if recommended_shell is not None: - recommended_install_command = shells[recommended_shell]['recommendedInstallCommand'] - - return { - 'ok': True, - 'message': 'completion 诊断信息已生成', - 'activeShell': active_shell or None, - 'projectDir': str(Path.cwd().resolve()), - 'envChoices': self.environment_option_service.discover_env_names(), - 'completeEnvVar': CLICK_COMPLETE_ENV_VAR, - 'recommendedInstallCommand': recommended_install_command, - 'shells': shells, - } - - -COMPLETION_DOCTOR = CompletionDoctorService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/installers.py b/shierfeng-fastapi-backend/build/lib/cli/completion/installers.py deleted file mode 100644 index d57050c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/installers.py +++ /dev/null @@ -1,465 +0,0 @@ -import io -import os -from contextlib import redirect_stderr -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -import click -import typer -import typer.main -from click.shell_completion import BashComplete, FishComplete, ZshComplete - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway -from cli.completion.shells import PowerShellComplete, ensure_custom_completion_classes_registered -from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR -from cli.metadata import COMPLETION_SHELL_SPEC_REGISTRY, CompletionShellSpec, CompletionShellSpecRegistry - -CLICK_COMPLETE_ENV_VAR = '_SHIERFENG_COMPLETE' - - -class CompletionInstallerShellSupport: - """ - completion shell 差异支持对象。 - - 该对象负责封装不同 shell 在脚本兼容处理和 source 命令构建上的差异, - 供安装服务通过轻量 strategy 协作对象统一复用。 - """ - - @staticmethod - def keep_script_text(script_text: str) -> str: - """ - 保持脚本文本原样返回。 - - :param script_text: 原始脚本文本 - :return: 原样脚本文本 - """ - return script_text - - @staticmethod - def make_bash_completion_script_compatible(script_text: str) -> str: - """ - 对 Click 生成的 Bash completion 脚本做旧版本兼容处理。 - - 兼容目标主要包括 macOS 默认 Bash 3.2: - - - `complete -o nosort` 在旧 Bash 上会在 `source` 阶段直接报错 - - `compopt` 在旧 Bash 上可能不存在 - - :param script_text: Click 原始 Bash completion 脚本文本 - :return: 兼容处理后的脚本文本 - """ - compatible_lines: list[str] = [] - for line in script_text.splitlines(): - stripped_line = line.strip() - if stripped_line == 'compopt -o dirnames': - indent = line[: len(line) - len(line.lstrip(' '))] - compatible_lines.extend( - [ - f'{indent}if command -v compopt >/dev/null 2>&1; then', - f'{indent} compopt -o dirnames', - f'{indent}fi', - ] - ) - continue - if stripped_line == 'compopt -o default': - indent = line[: len(line) - len(line.lstrip(' '))] - compatible_lines.extend( - [ - f'{indent}if command -v compopt >/dev/null 2>&1; then', - f'{indent} compopt -o default', - f'{indent}fi', - ] - ) - continue - if stripped_line.startswith('complete -o nosort -F '): - compatibility_line = stripped_line.replace('complete -o nosort -F ', '', 1) - function_name, command_name = compatibility_line.split(' ', 1) - compatible_lines.extend( - [ - f'if complete -o nosort -F {function_name} {command_name} 2>/dev/null; then', - ' :', - 'else', - f' complete -F {function_name} {command_name}', - 'fi', - ] - ) - continue - compatible_lines.append(line) - return '\n'.join(compatible_lines) + '\n' - - @staticmethod - def build_posix_source_command(target_file: Path) -> str: - """ - 构建 POSIX shell 使用的 source 命令。 - - :param target_file: completion 脚本文件路径 - :return: source 命令 - """ - return f'source {target_file}' - - @staticmethod - def build_fish_source_command(target_file: Path) -> str: - """ - 构建 Fish shell 使用的 source 命令。 - - :param target_file: completion 脚本文件路径 - :return: source 命令 - """ - return f'status --is-interactive; and source {target_file}' - - @staticmethod - def build_powershell_source_command(target_file: Path) -> str: - """ - 构建 PowerShell 使用的 source 命令。 - - :param target_file: completion 脚本文件路径 - :return: source 命令 - """ - return f'. "{target_file}"' - - -@dataclass(frozen=True) -class CompletionShellRuntimePolicy: - """ - completion shell 运行时策略定义。 - - :param name: shell 名称 - :param click_completion_class: Click completion 生成器类型 - :param script_transformer: 脚本文本后处理函数 - :param source_command_builder: 激活命令构建函数 - """ - - name: str - click_completion_class: type[Any] - script_transformer: Any - source_command_builder: Any - - -@dataclass(frozen=True) -class CompletionShellRuntimePolicyRegistry: - """ - completion shell 运行时策略注册表。 - - :param policies: 按 shell 名称索引的运行时策略映射 - """ - - policies: dict[str, CompletionShellRuntimePolicy] - - def get(self, shell_name: str) -> CompletionShellRuntimePolicy | None: - """ - 获取指定 shell 的运行时策略。 - - :param shell_name: shell 名称 - :return: 运行时策略,不存在时返回 None - """ - return self.policies.get(shell_name) - - -DEFAULT_COMPLETION_SHELL_RUNTIME_POLICIES = CompletionShellRuntimePolicyRegistry( - policies={ - 'bash': CompletionShellRuntimePolicy( - name='bash', - click_completion_class=BashComplete, - script_transformer=CompletionInstallerShellSupport.make_bash_completion_script_compatible, - source_command_builder=CompletionInstallerShellSupport.build_posix_source_command, - ), - 'zsh': CompletionShellRuntimePolicy( - name='zsh', - click_completion_class=ZshComplete, - script_transformer=CompletionInstallerShellSupport.keep_script_text, - source_command_builder=CompletionInstallerShellSupport.build_posix_source_command, - ), - 'fish': CompletionShellRuntimePolicy( - name='fish', - click_completion_class=FishComplete, - script_transformer=CompletionInstallerShellSupport.keep_script_text, - source_command_builder=CompletionInstallerShellSupport.build_fish_source_command, - ), - 'powershell': CompletionShellRuntimePolicy( - name='powershell', - click_completion_class=PowerShellComplete, - script_transformer=CompletionInstallerShellSupport.keep_script_text, - source_command_builder=CompletionInstallerShellSupport.build_powershell_source_command, - ), - } -) - - -class CompletionInstallerService: - """ - completion 安装与脚本生成服务。 - - 该服务负责 shell 元数据解析、脚本生成、目标路径解析、激活命令构建、 - rc 文件写入以及安装结果收口。 - - :param completion_provider_gateway: completion 提供器对外网关 - :param shell_spec_registry: shell 元数据注册表 - :param shell_runtime_policy_registry: shell 运行时策略注册表 - """ - - def __init__( - self, - *, - completion_provider_gateway: CompletionProviderGateway | None = None, - shell_spec_registry: CompletionShellSpecRegistry | None = None, - shell_runtime_policy_registry: CompletionShellRuntimePolicyRegistry | None = None, - ) -> None: - """ - 初始化 completion 安装服务。 - - :param completion_provider_gateway: completion 提供器对外网关 - :param shell_spec_registry: shell 元数据注册表 - :param shell_runtime_policy_registry: shell 运行时策略注册表 - :return: None - """ - self.completion_provider_gateway = completion_provider_gateway or COMPLETION_PROVIDER_GATEWAY - self.shell_spec_registry = shell_spec_registry or COMPLETION_SHELL_SPEC_REGISTRY - self.shell_runtime_policy_registry = shell_runtime_policy_registry or DEFAULT_COMPLETION_SHELL_RUNTIME_POLICIES - ensure_custom_completion_classes_registered() - - def resolve_completion_shell_spec(self, shell: str) -> CompletionShellSpec: - """ - 获取指定 shell 的 completion 元数据。 - - :param shell: shell 名称 - :return: shell 元数据 - :raises typer.BadParameter: shell 不存在时抛出异常 - """ - normalized_shell = shell.strip().lower() - shell_spec = self.shell_spec_registry.get_spec(normalized_shell) - if shell_spec is None: - supported_shells = ', '.join(self.completion_provider_gateway.list_completion_shells()) - raise typer.BadParameter(f'不支持的 shell:{shell},可选值为 {supported_shells}') - return shell_spec - - def resolve_shell_runtime_policy(self, shell: str) -> CompletionShellRuntimePolicy: - """ - 获取指定 shell 的运行时策略。 - - :param shell: shell 名称 - :return: shell 运行时策略 - :raises typer.BadParameter: 当前 shell 未实现运行时策略时抛出异常 - """ - shell_spec = self.resolve_completion_shell_spec(shell) - runtime_policy = self.shell_runtime_policy_registry.get(shell_spec.name) - if runtime_policy is None: - raise typer.BadParameter(f'{shell_spec.name} completion 当前版本未实现') - return runtime_policy - - @staticmethod - def build_completion_click_command(root_cli: typer.Typer) -> click.Command: - """ - 将 Typer 根应用转换为 Click 命令对象,供 shell completion 生成使用。 - - :param root_cli: Typer 根应用 - :return: Click 命令对象 - """ - return typer.main.get_command(root_cli) - - def render_completion_script(self, root_cli: typer.Typer, shell: str) -> str: - """ - 生成指定 shell 的 completion 脚本文本。 - - :param root_cli: Typer 根应用 - :param shell: shell 名称 - :return: completion 脚本文本 - :raises typer.BadParameter: 当前 shell 未实现脚本生成时抛出异常 - """ - shell_spec = self.resolve_completion_shell_spec(shell) - if not shell_spec.supported or shell_spec.generator not in {'click', 'custom'}: - raise typer.BadParameter(f'{shell_spec.name} completion 当前版本未实现') - - runtime_policy = self.resolve_shell_runtime_policy(shell_spec.name) - click_command = self.build_completion_click_command(root_cli) - stderr_buffer = io.StringIO() - with redirect_stderr(stderr_buffer): - script_text = runtime_policy.click_completion_class( - click_command, - {}, - 'shierfeng', - CLICK_COMPLETE_ENV_VAR, - ).source() - return runtime_policy.script_transformer(script_text) - - def resolve_completion_target(self, shell: str, target_file: Path | None = None) -> Path: - """ - 解析 completion 脚本目标文件路径。 - - :param shell: shell 名称 - :param target_file: 用户显式指定的目标文件 - :return: 目标文件绝对路径 - """ - if target_file is not None: - return target_file.expanduser().resolve() - - shell_spec = self.resolve_completion_shell_spec(shell) - return (Path.home() / shell_spec.default_target).expanduser().resolve() - - def resolve_completion_rc_file(self, shell: str, rc_file: Path | None = None) -> Path | None: - """ - 解析 completion 激活所使用的 rc 文件路径。 - - :param shell: shell 名称 - :param rc_file: 用户显式指定的 rc 文件 - :return: rc 文件绝对路径,若当前 shell 无 rc 文件则返回 None - """ - if rc_file is not None: - return rc_file.expanduser().resolve() - - shell_spec = self.resolve_completion_shell_spec(shell) - if shell_spec.default_rc_file is None: - return None - return (Path.home() / shell_spec.default_rc_file).expanduser().resolve() - - def build_source_command(self, target_file: Path, shell: str) -> str: - """ - 构建当前 shell 对应的激活命令。 - - :param target_file: completion 脚本文件路径 - :param shell: shell 名称 - :return: 激活命令文本 - """ - runtime_policy = self.resolve_shell_runtime_policy(shell) - return runtime_policy.source_command_builder(target_file) - - @staticmethod - def detect_active_shell() -> str: - """ - 检测当前进程环境下的活跃 shell 名称。 - - :return: shell 名称,未知时返回空字符串 - """ - shell_path = os.environ.get('SHELL', '').strip() - if not shell_path: - return '' - return Path(shell_path).name.lower() - - def resolve_install_shell(self, shell: str | None) -> str: - """ - 解析安装命令实际使用的 shell。 - - :param shell: 用户显式指定的 shell,允许为空 - :return: 实际使用的 shell 名称 - :raises typer.BadParameter: 无法推断或不支持时抛出异常 - """ - if shell and shell.strip(): - return self.resolve_completion_shell_spec(shell).name - - active_shell = self.detect_active_shell() - if not active_shell: - raise typer.BadParameter('未检测到当前 shell,请显式传入 --shell') - - shell_spec = self.shell_spec_registry.get_spec(active_shell) - if shell_spec is None: - supported_shells = ', '.join(self.completion_provider_gateway.list_completion_shells()) - raise typer.BadParameter( - f'当前 shell `{active_shell}` 不在支持列表中,请显式传入 --shell,可选值为 {supported_shells}' - ) - return shell_spec.name - - @staticmethod - def append_activation_line(rc_file: Path, source_command: str) -> bool: - """ - 将激活命令追加到 rc 文件,若已存在则不重复写入。 - - :param rc_file: rc 文件路径 - :param source_command: 激活命令文本 - :return: 本次是否发生写入 - """ - existing_text = '' - if rc_file.exists(): - existing_text = rc_file.read_text(encoding='utf-8') - if source_command in existing_text: - return False - - rc_file.parent.mkdir(parents=True, exist_ok=True) - with rc_file.open('a', encoding='utf-8') as file_object: - if existing_text and not existing_text.endswith('\n'): - file_object.write('\n') - file_object.write(f'{source_command}\n') - return True - - def install_completion_script( - self, - root_cli: typer.Typer, - shell: str | None, - *, - target_file: Path | None = None, - activate: bool = False, - rc_file: Path | None = None, - force: bool = False, - ) -> dict[str, Any]: - """ - 安装指定 shell 的 completion 脚本文件。 - - :param root_cli: Typer 根应用 - :param shell: shell 名称 - :param target_file: completion 脚本目标文件 - :param activate: 是否写入 rc 文件激活命令 - :param rc_file: 自定义 rc 文件路径 - :param force: 目标文件已存在时是否强制覆盖 - :return: 安装结果字典 - """ - resolved_shell = self.resolve_install_shell(shell) - shell_spec = self.resolve_completion_shell_spec(resolved_shell) - if not shell_spec.supported: - return { - 'ok': False, - 'message': f'{shell_spec.name} completion 当前版本未实现', - 'shell': shell_spec.name, - 'exit_code': ARGUMENT_ERROR, - } - - resolved_target_file = self.resolve_completion_target(shell_spec.name, target_file) - script_text = self.render_completion_script(root_cli, shell_spec.name) - existing_text = resolved_target_file.read_text(encoding='utf-8') if resolved_target_file.exists() else None - - if existing_text is not None and existing_text != script_text and not force: - return { - 'ok': False, - 'message': '目标文件已存在且内容不同,请传入 --force 覆盖', - 'shell': shell_spec.name, - 'targetFile': str(resolved_target_file), - 'exit_code': RUNTIME_ERROR, - } - - resolved_target_file.parent.mkdir(parents=True, exist_ok=True) - resolved_target_file.write_text(script_text, encoding='utf-8') - - source_command = self.build_source_command(resolved_target_file, shell_spec.name) - activated = shell_spec.auto_discovery - rc_file_path = self.resolve_completion_rc_file(shell_spec.name, rc_file) - rc_file_updated = False - activation_required = not shell_spec.auto_discovery - detected_shell = self.detect_active_shell() or None - - if activate and rc_file_path is not None: - rc_file_updated = self.append_activation_line(rc_file_path, source_command) - activated = True - - next_step = '当前 shell 会自动发现 completion 脚本,无需额外 source 命令' - if activation_required and not activate: - next_step = f'请执行 `{source_command}`,或重新运行并传入 --activate' - elif activation_required and activate: - next_step = '请重启当前 shell,或手动执行 rc 文件中的 source 命令使其立即生效' - - return { - 'ok': True, - 'message': 'completion 脚本已安装', - 'shell': shell_spec.name, - 'detectedShell': detected_shell, - 'targetFile': str(resolved_target_file), - 'activated': activated, - 'activateRequested': activate, - 'rcFile': str(rc_file_path) if rc_file_path is not None else None, - 'rcFileUpdated': rc_file_updated, - 'sourceCommand': source_command, - 'autoDiscovery': shell_spec.auto_discovery, - 'activationRequired': activation_required, - 'nextStep': next_step, - 'completeEnvVar': CLICK_COMPLETE_ENV_VAR, - } - - -COMPLETION_INSTALLER = CompletionInstallerService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/completion/presenter.py deleted file mode 100644 index f15667f..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/presenter.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Any - - -class CompletionCommandPresenter: - """ - completion 命令文本渲染器。 - - 该渲染器负责将 `completion` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_completion_doctor_text(self, payload: dict[str, Any]) -> str: - """ - 将 completion 诊断结果渲染为文本摘要。 - - :param payload: completion 诊断结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'message: {payload.get("message", "-")}', - f'active_shell: {payload.get("activeShell") or "-"}', - f'project_dir: {payload.get("projectDir", "-")}', - f'complete_env_var: {payload.get("completeEnvVar", "-")}', - ] - if payload.get('recommendedInstallCommand'): - lines.append(f'recommended_install_command: {payload.get("recommendedInstallCommand")}') - env_choices = payload.get('envChoices') - if isinstance(env_choices, list) and env_choices: - lines.append('env_choices:') - lines.extend(f' - {env_name}' for env_name in env_choices) - shells = payload.get('shells') - if isinstance(shells, dict) and shells: - lines.append('shells:') - for shell_name, shell_payload in shells.items(): - if not isinstance(shell_payload, dict): - continue - lines.extend( - [ - f' {shell_name}:', - f' supported: {str(shell_payload.get("supported", False)).lower()}', - f' detected: {str(shell_payload.get("detected", False)).lower()}', - f' target_file: {shell_payload.get("targetFile", "-")}', - f' target_file_exists: {str(shell_payload.get("targetFileExists", False)).lower()}', - f' rc_file: {shell_payload.get("rcFile", "-") or "-"}', - f' rc_file_exists: {str(shell_payload.get("rcFileExists", False)).lower()}', - f' auto_discovery: {str(shell_payload.get("autoDiscovery", False)).lower()}', - f' source_command: {shell_payload.get("sourceCommand", "-")}', - f' recommended_install_command: {shell_payload.get("recommendedInstallCommand", "-")}', - ] - ) - return '\n'.join(lines) - - @staticmethod - def build_completion_install_text(payload: dict[str, Any]) -> str: - """ - 将 completion 安装结果渲染为文本摘要。 - - :param payload: completion 安装结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'message: {payload.get("message", "-")}', - f'shell: {payload.get("shell", "-")}', - ] - field_label_mapping = { - 'detectedShell': 'detected_shell', - 'targetFile': 'target_file', - 'activated': 'activated', - 'activateRequested': 'activate_requested', - 'rcFile': 'rc_file', - 'rcFileUpdated': 'rc_file_updated', - 'sourceCommand': 'source_command', - 'autoDiscovery': 'auto_discovery', - 'activationRequired': 'activation_required', - 'nextStep': 'next_step', - 'completeEnvVar': 'complete_env_var', - } - for field_name, field_label in field_label_mapping.items(): - if field_name in payload: - lines.append(f'{field_label}: {payload.get(field_name)}') - return '\n'.join(lines) diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/providers.py b/shierfeng-fastapi-backend/build/lib/cli/completion/providers.py deleted file mode 100644 index 1b9dadf..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/providers.py +++ /dev/null @@ -1,1022 +0,0 @@ -import asyncio -import os -from dataclasses import dataclass, field -from importlib import import_module -from pathlib import Path -from typing import Any - -import click - -from cli.metadata import ( - COMPLETION_SHELL_SPEC_REGISTRY, - ENVIRONMENT_OPTION_SERVICE, - CompletionShellSpecRegistry, - EnvironmentOptionService, -) - -DEFAULT_ALEMBIC_REVISION_CHOICES = ('head', 'base', 'current', '-1') -DYNAMIC_COMPLETION_TIMEOUT_SECONDS = 0.8 - - -@dataclass(frozen=True) -class CompletionContextResolver: - """ - shell completion 上下文解析器。 - - 该对象负责解析当前补全场景下的环境、缓存名称和项目目录, - 将原本散落的参数与环境变量解析逻辑收口到单一职责对象中。 - """ - - def resolve_completion_env(self, ctx: click.Context | None) -> str: - """ - 解析当前 completion 场景使用的环境名称。 - - :param ctx: Click 上下文 - :return: 环境名称,默认返回 `dev` - """ - if ctx is not None: - env_value = getattr(ctx, 'params', {}).get('env') - if isinstance(env_value, str) and env_value.strip(): - return env_value.strip() - - comp_words = os.environ.get('COMP_WORDS', '').strip() - if comp_words: - tokens = comp_words.split() - for index, token in enumerate(tokens): - if token.startswith('--env='): - env_value = token.split('=', 1)[1].strip() - if env_value: - return env_value - if token == '--env' and index + 1 < len(tokens): - env_value = tokens[index + 1].strip() - if env_value: - return env_value - - return 'dev' - - def resolve_cache_name_for_completion(self, ctx: click.Context | None) -> str: - """ - 解析当前 cache key 补全场景使用的缓存名称。 - - :param ctx: Click 上下文 - :return: 缓存名称,缺失时返回空字符串 - """ - if ctx is not None: - cache_name = getattr(ctx, 'params', {}).get('cache_name') - if isinstance(cache_name, str) and cache_name.strip(): - return cache_name.strip() - - comp_words = os.environ.get('COMP_WORDS', '').strip() - if not comp_words: - return '' - - tokens = comp_words.split() - try: - cache_index = tokens.index('cache') - except ValueError: - return '' - - if cache_index + 2 >= len(tokens): - return '' - - subcommand = tokens[cache_index + 1] - if subcommand not in {'get', 'ttl'}: - return '' - - cache_name = tokens[cache_index + 2].strip() - if cache_name.startswith('-'): - return '' - return cache_name - - @staticmethod - def resolve_project_dir() -> Path: - """ - 获取当前 CLI 工作目录对应的项目根目录。 - - :return: 当前项目根目录 - """ - return Path.cwd().resolve() - - @staticmethod - def normalize_completion_prefix(incomplete: str) -> str: - """ - 规范化补全输入前缀。 - - :param incomplete: 原始未完成输入 - :return: 规范化后的前缀文本 - """ - return incomplete.strip() - - @staticmethod - def to_display_path(path: Path, *, project_dir: Path) -> str: - """ - 将绝对路径转换为适合补全显示的路径文本。 - - :param path: 原始路径 - :param project_dir: 项目根目录 - :return: 用于 shell completion 的显示路径 - """ - try: - relative_path = path.relative_to(project_dir) - return str(relative_path) or '.' - except ValueError: - return str(path) - - -@dataclass -class DynamicCompletionService: - """ - 动态补全执行服务。 - - 该服务负责 runtime 模块导入、只读协程执行、标准 payload 提取与 - 候选过滤,从而避免各补全函数继续复制相同的模板。 - - :param context_resolver: completion 上下文解析器 - :param timeout_seconds: 动态补全超时时间 - """ - - context_resolver: CompletionContextResolver = field(default_factory=CompletionContextResolver) - timeout_seconds: float = DYNAMIC_COMPLETION_TIMEOUT_SECONDS - - @staticmethod - def _scoped_app_env(env: str) -> Any: - """ - 为动态补全临时注入 `APP_ENV` 环境变量。 - - `config.env` 在导入阶段会优先读取 `APP_ENV`,因此补全场景下 - 直接切换进程环境变量比改写 `sys.argv` 更稳定,也更符合当前 - 后端配置模块的解析约束。 - - :param env: 当前补全环境名称 - :return: 环境变量恢复上下文 - """ - - class _CompletionEnvScope: - def __enter__(self) -> None: - self._original_app_env = os.environ.get('APP_ENV') - os.environ['APP_ENV'] = env - - def __exit__(self, exc_type: object, exc: object, traceback: object) -> None: - if self._original_app_env is None: - os.environ.pop('APP_ENV', None) - else: - os.environ['APP_ENV'] = self._original_app_env - - return _CompletionEnvScope() - - def load_runtime_module(self, module_name: str) -> Any: - """ - 加载动态补全依赖的 runtime 模块。 - - :param module_name: runtime 模块名 - :return: 已导入模块 - """ - return import_module(module_name) - - def run_completion_coroutine(self, coroutine: Any, *, env: str) -> Any: - """ - 在补全场景下执行异步只读查询,并临时注入 `APP_ENV`。 - - :param coroutine: 待执行协程 - :param env: 当前补全环境名称 - :return: 协程执行结果 - """ - try: - with self._scoped_app_env(env): - return asyncio.run(asyncio.wait_for(coroutine, timeout=self.timeout_seconds)) - finally: - if asyncio.iscoroutine(coroutine): - coroutine.close() - - @staticmethod - def extract_completion_items(payload: dict[str, Any], field_name: str) -> list[str]: - """ - 从 CLI 标准结果中提取补全候选字段列表。 - - :param payload: CLI 标准结果字典 - :param field_name: 需要提取的字段名 - :return: 去重后的字符串列表 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return [] - - items = payload.get('items') - if not isinstance(items, list): - page_payload = payload.get('page') - if isinstance(page_payload, dict): - items = page_payload.get('rows') - if not isinstance(items, list): - return [] - - candidates = [] - for item in items: - if not isinstance(item, dict): - continue - value = item.get(field_name) - if isinstance(value, str) and value.strip(): - candidates.append(value.strip()) - return sorted(set(candidates)) - - @staticmethod - def extract_completion_values(payload: dict[str, Any], field_name: str) -> list[str]: - """ - 从 CLI 标准结果中提取字符串或整数类型的补全候选值。 - - :param payload: CLI 标准结果字典 - :param field_name: 需要提取的字段名 - :return: 去重后的字符串列表 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return [] - - items = payload.get('items') - if not isinstance(items, list): - page_payload = payload.get('page') - if isinstance(page_payload, dict): - items = page_payload.get('rows') - if not isinstance(items, list): - return [] - - candidates = [] - for item in items: - if not isinstance(item, dict): - continue - value = item.get(field_name) - if isinstance(value, str) and value.strip(): - candidates.append(value.strip()) - elif isinstance(value, int): - candidates.append(str(value)) - return sorted(set(candidates)) - - @staticmethod - def extract_completion_list(payload: dict[str, Any], field_name: str) -> list[str]: - """ - 从 CLI 标准结果中提取直接位于顶层字段的字符串列表。 - - 该方法用于缓存键名等不走 `items/page.rows` 结构的场景, - 使这类动态补全也能统一复用 runtime 对象加载与协程执行逻辑。 - - :param payload: CLI 标准结果字典 - :param field_name: 需要提取的顶层字段名 - :return: 去重后的字符串列表 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return [] - - items = payload.get(field_name) - if not isinstance(items, list): - return [] - - candidates = [item.strip() for item in items if isinstance(item, str) and item.strip()] - return sorted(set(candidates)) - - @staticmethod - def filter_candidates(candidates: list[str], incomplete: str) -> list[str]: - """ - 按未完成输入前缀过滤候选值。 - - :param candidates: 原始候选列表 - :param incomplete: 当前未完成输入 - :return: 过滤后的候选列表 - """ - normalized_incomplete = incomplete.strip().lower() - return [candidate for candidate in candidates if candidate.lower().startswith(normalized_incomplete)] - - def complete_dynamic_items( - self, - ctx: click.Context | None, - incomplete: str, - *, - runtime_module_name: str, - runtime_object_name: str | None = None, - runtime_method_name: str, - runtime_kwargs: dict[str, Any], - field_name: str, - allow_int_values: bool = False, - ) -> list[str]: - """ - 执行通用的动态只读补全查询。 - - :param ctx: Click 上下文 - :param incomplete: 当前未完成输入 - :param runtime_module_name: runtime 模块名 - :param runtime_object_name: runtime 对象名,为空时直接使用模块 - :param runtime_method_name: runtime 方法名 - :param runtime_kwargs: runtime 调用参数 - :param field_name: 待提取字段名 - :param allow_int_values: 是否允许整数候选 - :return: 过滤后的候选列表 - """ - env = self.context_resolver.resolve_completion_env(ctx) - try: - runtime_module = self.load_runtime_module(runtime_module_name) - runtime_target = getattr(runtime_module, runtime_object_name) if runtime_object_name else runtime_module - runtime_method = getattr(runtime_target, runtime_method_name) - payload = self.run_completion_coroutine(runtime_method(**runtime_kwargs), env=env) - except Exception: - return [] - - if allow_int_values: - candidates = self.extract_completion_values(payload, field_name) - else: - candidates = self.extract_completion_items(payload, field_name) - return self.filter_candidates(candidates, incomplete) - - def complete_dynamic_list( - self, - ctx: click.Context | None, - incomplete: str, - *, - runtime_module_name: str, - runtime_object_name: str | None = None, - runtime_method_name: str, - runtime_args: tuple[Any, ...] = (), - runtime_kwargs: dict[str, Any] | None = None, - field_name: str, - ) -> list[str]: - """ - 执行返回顶层字符串列表字段的动态只读补全查询。 - - :param ctx: Click 上下文 - :param incomplete: 当前未完成输入 - :param runtime_module_name: runtime 模块名 - :param runtime_object_name: runtime 对象名,为空时直接使用模块 - :param runtime_method_name: runtime 方法名 - :param runtime_args: runtime 位置参数 - :param runtime_kwargs: runtime 关键字参数 - :param field_name: 顶层列表字段名 - :return: 过滤后的候选列表 - """ - env = self.context_resolver.resolve_completion_env(ctx) - try: - runtime_module = self.load_runtime_module(runtime_module_name) - runtime_target = getattr(runtime_module, runtime_object_name) if runtime_object_name else runtime_module - runtime_method = getattr(runtime_target, runtime_method_name) - payload = self.run_completion_coroutine( - runtime_method(*runtime_args, **(runtime_kwargs or {})), - env=env, - ) - except Exception: - return [] - - candidates = self.extract_completion_list(payload, field_name) - return self.filter_candidates(candidates, incomplete) - - -@dataclass -class StaticCompletionProvider: - """ - 静态 completion 提供器。 - - 该对象负责 shell、环境、缓存名称以及 Alembic revision 等静态或 - 本地推导型补全候选。 - - :param context_resolver: completion 上下文解析器 - :param dynamic_service: 动态补全执行服务 - :param shell_spec_registry: shell 元数据注册表 - :param environment_option_service: 环境选项服务 - """ - - context_resolver: CompletionContextResolver = field(default_factory=CompletionContextResolver) - dynamic_service: DynamicCompletionService = field( - default_factory=lambda: DynamicCompletionService(context_resolver=CompletionContextResolver()) - ) - shell_spec_registry: CompletionShellSpecRegistry = field(default_factory=lambda: COMPLETION_SHELL_SPEC_REGISTRY) - environment_option_service: EnvironmentOptionService = field(default_factory=lambda: ENVIRONMENT_OPTION_SERVICE) - - def list_completion_shells(self) -> list[str]: - """ - 获取已声明的 completion shell 列表。 - - :return: shell 名称列表 - """ - return self.shell_spec_registry.list_shell_names() - - def list_static_cache_names(self) -> list[str]: - """ - 读取系统内置缓存名称列表。 - - :return: 缓存名称列表 - """ - try: - redis_init_key_config = self.dynamic_service.load_runtime_module('common.enums').RedisInitKeyConfig - except Exception: - return [] - return [key_config.key for key_config in redis_init_key_config if getattr(key_config, 'key', '')] - - def complete_env_values( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 `--env` 选项提供可补全的环境名称。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 匹配的环境名称列表 - """ - del ctx, args - normalized_incomplete = incomplete.strip().lower() - return [ - env_name - for env_name in self.environment_option_service.discover_env_names() - if env_name.lower().startswith(normalized_incomplete) - ] - - def complete_shell_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 shell 参数提供静态补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: shell 名称列表 - """ - del ctx, args - normalized_incomplete = incomplete.strip().lower() - return [ - shell_name for shell_name in self.list_completion_shells() if shell_name.startswith(normalized_incomplete) - ] - - def complete_cache_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为缓存名称参数提供静态补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 缓存名称列表 - """ - del ctx, args - normalized_incomplete = incomplete.strip().lower() - return [ - cache_name - for cache_name in sorted(self.list_static_cache_names()) - if cache_name.lower().startswith(normalized_incomplete) - ] - - def complete_alembic_revisions( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为数据库迁移版本参数提供本地补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 迁移版本候选列表 - """ - del ctx, args - normalized_incomplete = self.context_resolver.normalize_completion_prefix(incomplete).lower() - revision_choices = set(DEFAULT_ALEMBIC_REVISION_CHOICES) - versions_dir = self.context_resolver.resolve_project_dir() / 'alembic' / 'versions' - if versions_dir.is_dir(): - for revision_file in versions_dir.glob('*.py'): - revision_stem = revision_file.stem - revision_id = revision_stem.split('-', 1)[-1].split('_', 1)[0].strip() - if revision_id: - revision_choices.add(revision_id) - return sorted(choice for choice in revision_choices if choice.lower().startswith(normalized_incomplete)) - - -@dataclass -class PathCompletionProvider: - """ - 路径 completion 提供器。 - - 该对象负责项目相对 SQL 文件路径和输出路径补全。 - - :param context_resolver: completion 上下文解析器 - """ - - context_resolver: CompletionContextResolver = field(default_factory=CompletionContextResolver) - - def complete_sql_files( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 SQL 文件参数提供项目内 `.sql` 文件补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: SQL 文件路径列表 - """ - del ctx, args - normalized_incomplete = self.context_resolver.normalize_completion_prefix(incomplete).lower() - project_dir = self.context_resolver.resolve_project_dir() - candidates = [] - for sql_file in project_dir.rglob('*.sql'): - if '.git' in sql_file.parts or '__pycache__' in sql_file.parts: - continue - display_path = self.context_resolver.to_display_path(sql_file, project_dir=project_dir) - if display_path.lower().startswith(normalized_incomplete): - candidates.append(display_path) - return sorted(candidates) - - def complete_output_paths( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为导出文件参数提供目录和 `.zip` 文件补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 目录或 zip 文件路径列表 - """ - del ctx, args - raw_incomplete = self.context_resolver.normalize_completion_prefix(incomplete) - project_dir = self.context_resolver.resolve_project_dir() - - expanded_input = Path(raw_incomplete).expanduser() - input_is_absolute = expanded_input.is_absolute() - if raw_incomplete.endswith(('/', os.sep)): - search_dir = expanded_input if input_is_absolute else (project_dir / expanded_input).resolve() - partial_name = '' - else: - search_dir = ( - expanded_input.parent - if input_is_absolute - else ( - project_dir / expanded_input.parent if str(expanded_input.parent) != '.' else project_dir - ).resolve() - ) - partial_name = expanded_input.name - - if not search_dir.exists() or not search_dir.is_dir(): - return [] - - candidates = [] - for child_path in sorted(search_dir.iterdir()): - if not child_path.name.startswith(partial_name): - continue - if child_path.is_dir(): - display_path = self.context_resolver.to_display_path(child_path, project_dir=project_dir) - candidates.append(f'{display_path}/') - continue - if child_path.suffix.lower() == '.zip': - candidates.append(self.context_resolver.to_display_path(child_path, project_dir=project_dir)) - return candidates - - -@dataclass -class DomainDynamicCompletionProvider: - """ - 业务域动态 completion 提供器。 - - 该对象负责配置、代码生成、缓存、任务等运行时驱动的只读补全。 - - :param context_resolver: completion 上下文解析器 - :param dynamic_service: 动态补全执行服务 - """ - - context_resolver: CompletionContextResolver = field(default_factory=CompletionContextResolver) - dynamic_service: DynamicCompletionService = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化动态补全执行服务。 - - :return: None - """ - self.dynamic_service = DynamicCompletionService(context_resolver=self.context_resolver) - - def complete_config_keys( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为参数键名提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 参数键名列表 - """ - del args - return self.dynamic_service.complete_dynamic_items( - ctx, - incomplete, - runtime_module_name='cli.runtime.config', - runtime_object_name='CONFIG_RUNTIME', - runtime_method_name='list_configs', - runtime_kwargs={'config_key': incomplete, 'paged': True, 'page_num': 1, 'page_size': 20}, - field_name='configKey', - ) - - def complete_gen_table_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为代码生成业务表名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 业务表名称列表 - """ - del args - return self.dynamic_service.complete_dynamic_items( - ctx, - incomplete, - runtime_module_name='cli.runtime.gen', - runtime_object_name='GEN_RUNTIME', - runtime_method_name='list_gen_tables', - runtime_kwargs={'table_name': incomplete, 'paged': True, 'page_num': 1, 'page_size': 20}, - field_name='tableName', - ) - - def complete_gen_db_table_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为数据库物理表名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 数据库物理表名称列表 - """ - del args - return self.dynamic_service.complete_dynamic_items( - ctx, - incomplete, - runtime_module_name='cli.runtime.gen', - runtime_object_name='GEN_RUNTIME', - runtime_method_name='list_gen_db_tables', - runtime_kwargs={'table_name': incomplete, 'paged': True, 'page_num': 1, 'page_size': 20}, - field_name='tableName', - ) - - def complete_cache_keys( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为缓存键名提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 缓存键名列表 - """ - del args - cache_name = self.context_resolver.resolve_cache_name_for_completion(ctx) - if not cache_name: - return [] - return self.dynamic_service.complete_dynamic_list( - ctx, - incomplete, - runtime_module_name='cli.runtime.cache', - runtime_object_name='CACHE_RUNTIME', - runtime_method_name='list_cache_keys', - runtime_args=(cache_name,), - field_name='keys', - ) - - def complete_job_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为任务名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 任务名称列表 - """ - del args - return self.dynamic_service.complete_dynamic_items( - ctx, - incomplete, - runtime_module_name='cli.runtime.job', - runtime_object_name='JOB_RUNTIME', - runtime_method_name='list_jobs', - runtime_kwargs={'job_name': incomplete, 'paged': True, 'page_num': 1, 'page_size': 20}, - field_name='jobName', - ) - - def complete_job_ids( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为任务 ID 提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 任务 ID 列表 - """ - del args - return self.dynamic_service.complete_dynamic_items( - ctx, - incomplete, - runtime_module_name='cli.runtime.job', - runtime_object_name='JOB_RUNTIME', - runtime_method_name='list_jobs', - runtime_kwargs={'paged': True, 'page_num': 1, 'page_size': 20}, - field_name='jobId', - allow_int_values=True, - ) - - -@dataclass -class CompletionProviderRegistry: - """ - completion 提供器注册表。 - - 该注册表只负责持有静态补全、路径补全与动态只读补全 provider, - 供上层 gateway、安装器与其他协作者按职责选择使用。 - - :param context_resolver: completion 上下文解析器 - :param dynamic_service: 动态补全执行服务 - :param static_provider: 静态 completion 提供器 - :param path_provider: 路径 completion 提供器 - :param domain_provider: 业务域动态 completion 提供器 - """ - - context_resolver: CompletionContextResolver = field(default_factory=CompletionContextResolver) - dynamic_service: DynamicCompletionService = field(init=False) - static_provider: StaticCompletionProvider = field(init=False) - path_provider: PathCompletionProvider = field(init=False) - domain_provider: DomainDynamicCompletionProvider = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化聚合 provider 依赖。 - - :return: None - """ - self.dynamic_service = DynamicCompletionService(context_resolver=self.context_resolver) - self.static_provider = StaticCompletionProvider( - context_resolver=self.context_resolver, - dynamic_service=self.dynamic_service, - ) - self.path_provider = PathCompletionProvider(context_resolver=self.context_resolver) - self.domain_provider = DomainDynamicCompletionProvider(context_resolver=self.context_resolver) - self.domain_provider.dynamic_service = self.dynamic_service - - -@dataclass -class CompletionProviderGateway: - """ - completion 对外网关。 - - 该对象负责对命令声明层、上下文层和 TUI 层暴露统一的 completion 入口, - 避免 `CompletionProviderRegistry` 本体继续膨胀为大而全的委托门面。 - - :param provider_registry: completion provider 注册表 - """ - - provider_registry: CompletionProviderRegistry = field(default_factory=CompletionProviderRegistry) - - def list_completion_shells(self) -> list[str]: - """ - 获取已声明的 completion shell 列表。 - - :return: shell 名称列表 - """ - return self.provider_registry.static_provider.list_completion_shells() - - def list_static_cache_names(self) -> list[str]: - """ - 读取系统内置缓存名称列表。 - - :return: 缓存名称列表 - """ - return self.provider_registry.static_provider.list_static_cache_names() - - def complete_env_values( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 `--env` 选项提供可补全的环境名称。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 匹配的环境名称列表 - """ - return self.provider_registry.static_provider.complete_env_values(ctx, args, incomplete) - - def complete_shell_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 shell 参数提供静态补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: shell 名称列表 - """ - return self.provider_registry.static_provider.complete_shell_names(ctx, args, incomplete) - - def complete_cache_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为缓存名称参数提供静态补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 缓存名称列表 - """ - return self.provider_registry.static_provider.complete_cache_names(ctx, args, incomplete) - - def complete_alembic_revisions( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为数据库迁移版本参数提供本地补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 迁移版本候选列表 - """ - return self.provider_registry.static_provider.complete_alembic_revisions(ctx, args, incomplete) - - def complete_sql_files( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为 SQL 文件参数提供项目内 `.sql` 文件补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: SQL 文件路径列表 - """ - return self.provider_registry.path_provider.complete_sql_files(ctx, args, incomplete) - - def complete_output_paths( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为导出文件参数提供目录和 `.zip` 文件补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 目录或 zip 文件路径列表 - """ - return self.provider_registry.path_provider.complete_output_paths(ctx, args, incomplete) - - def complete_config_keys( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为参数键名提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 参数键名列表 - """ - return self.provider_registry.domain_provider.complete_config_keys(ctx, args, incomplete) - - def complete_gen_table_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为代码生成业务表名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 业务表名称列表 - """ - return self.provider_registry.domain_provider.complete_gen_table_names(ctx, args, incomplete) - - def complete_gen_db_table_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为数据库物理表名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 数据库物理表名称列表 - """ - return self.provider_registry.domain_provider.complete_gen_db_table_names(ctx, args, incomplete) - - def complete_cache_keys( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为缓存键名提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 缓存键名列表 - """ - return self.provider_registry.domain_provider.complete_cache_keys(ctx, args, incomplete) - - def complete_job_names( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为任务名称提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 任务名称列表 - """ - return self.provider_registry.domain_provider.complete_job_names(ctx, args, incomplete) - - def complete_job_ids( - self, - ctx: click.Context | None, - args: list[str] | None, - incomplete: str, - ) -> list[str]: - """ - 为任务 ID 提供动态只读补全结果。 - - :param ctx: Click 上下文 - :param args: 当前命令参数列表 - :param incomplete: 当前未完成输入片段 - :return: 任务 ID 列表 - """ - return self.provider_registry.domain_provider.complete_job_ids(ctx, args, incomplete) - - -COMPLETION_PROVIDER_REGISTRY = CompletionProviderRegistry() -COMPLETION_PROVIDER_GATEWAY = CompletionProviderGateway(provider_registry=COMPLETION_PROVIDER_REGISTRY) diff --git a/shierfeng-fastapi-backend/build/lib/cli/completion/shells.py b/shierfeng-fastapi-backend/build/lib/cli/completion/shells.py deleted file mode 100644 index 1bd8fc5..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/completion/shells.py +++ /dev/null @@ -1,113 +0,0 @@ -import os - -from click.shell_completion import ( - CompletionItem, - ShellComplete, - add_completion_class, - get_completion_class, - split_arg_string, -) - -_SOURCE_POWERSHELL = """\ -$%(complete_func)s = { - param($wordToComplete, $commandAst, $cursorPosition) - - $previousCompWords = $env:COMP_WORDS - $previousCompCword = $env:COMP_CWORD - $previousCompleteInstruction = $env:%(complete_var)s - - $commandLine = $commandAst.ToString() - if ($cursorPosition -lt $commandLine.Length) { - $commandLine = $commandLine.Substring(0, $cursorPosition) - } - - $env:COMP_WORDS = $commandLine - $env:COMP_CWORD = $wordToComplete - $env:%(complete_var)s = "powershell_complete" - - try { - %(prog_name)s | ForEach-Object { - $line = $_.ToString() - if ([string]::IsNullOrWhiteSpace($line)) { - return - } - $parts = $line -split "`t", 3 - $completionValue = if ($parts.Length -ge 2) { $parts[1] } else { "" } - $completionHelp = if ($parts.Length -ge 3 -and $parts[2]) { $parts[2] } else { $completionValue } - [System.Management.Automation.CompletionResult]::new( - $completionValue, - $completionValue, - [System.Management.Automation.CompletionResultType]::ParameterValue, - $completionHelp - ) - } - } finally { - if ($null -ne $previousCompWords) { - $env:COMP_WORDS = $previousCompWords - } else { - Remove-Item Env:\\COMP_WORDS -ErrorAction SilentlyContinue - } - if ($null -ne $previousCompCword) { - $env:COMP_CWORD = $previousCompCword - } else { - Remove-Item Env:\\COMP_CWORD -ErrorAction SilentlyContinue - } - if ($null -ne $previousCompleteInstruction) { - $env:%(complete_var)s = $previousCompleteInstruction - } else { - Remove-Item Env:\\%(complete_var)s -ErrorAction SilentlyContinue - } - } -} - -Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $%(complete_func)s -""" - - -class PowerShellComplete(ShellComplete): - """ - PowerShell shell completion 支持。 - - 基于 PowerShell `Register-ArgumentCompleter -Native` 协议,将当前 - 命令行和光标位置传回 Click completion 分发器,再把返回的候选项转换为 - `CompletionResult` 对象。 - """ - - name = 'powershell' - source_template = _SOURCE_POWERSHELL - - def get_completion_args(self) -> tuple[list[str], str]: - """ - 从 PowerShell 注入的环境变量中恢复 CLI 上下文。 - - :return: 已解析的完整参数与当前不完整输入 - """ - cwords = split_arg_string(os.environ.get('COMP_WORDS', '')) - incomplete = os.environ.get('COMP_CWORD', '') - if incomplete: - incomplete_parts = split_arg_string(incomplete) - incomplete = incomplete_parts[0] if incomplete_parts else incomplete - args = cwords[1:] - if incomplete and args and args[-1] == incomplete: - args.pop() - return args, incomplete - - def format_completion(self, item: CompletionItem) -> str: - """ - 将候选项格式化为 PowerShell 脚本可解析的文本行。 - - :param item: Click completion 候选项 - :return: 格式化后的文本 - """ - help_text = item.help or item.value - return f'{item.type}\t{item.value}\t{help_text}' - - -def ensure_custom_completion_classes_registered() -> None: - """ - 确保自定义 shell completion 类已注册到 Click。 - - :return: None - """ - if get_completion_class(PowerShellComplete.name) is None: - add_completion_class(PowerShellComplete) diff --git a/shierfeng-fastapi-backend/build/lib/cli/context.py b/shierfeng-fastapi-backend/build/lib/cli/context.py deleted file mode 100644 index adbfab9..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/context.py +++ /dev/null @@ -1,74 +0,0 @@ -from dataclasses import dataclass -from typing import Annotated, Literal - -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY - - -@dataclass -class CliContext: - """ - CLI 运行上下文。 - - env: 当前命令使用的环境名称 - output: 结果输出格式 - operator: 操作者标识,后续用于审计 - allow_prod: 是否显式允许生产环境执行危险命令 - yes: 是否跳过二次确认 - dry_run: 是否执行演练模式 - """ - - env: str = 'dev' - output: Literal['text', 'json'] = 'text' - operator: str | None = None - allow_prod: bool = False - yes: bool = False - dry_run: bool = False - - -class CliContextBuilder: - """ - CLI 上下文构建器。 - - 该对象负责将命令层显式参数收口为统一的 `CliContext` 数据对象, - 供上下文工厂与测试场景复用。 - """ - - @staticmethod - def build( - env: str, - output: Literal['text', 'json'], - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> CliContext: - """ - 构建命令执行上下文。 - - :param env: 当前命令环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过二次确认 - :param dry_run: 是否执行演练模式 - :return: 构建完成的 CLI 上下文 - """ - return CliContext( - env=env, - output=output, - allow_prod=allow_prod, - yes=yes, - dry_run=dry_run, - ) - - -CLI_CONTEXT_BUILDER = CliContextBuilder() - -EnvOption = Annotated[ - str, - typer.Option('--env', help='运行环境名称', autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_env_values), -] -OutputOption = Annotated[Literal['text', 'json'], typer.Option('--output', help='输出格式,支持 text 或 json')] -AllowProdOption = Annotated[bool, typer.Option('--allow-prod', help='允许在生产环境中执行危险命令')] -YesOption = Annotated[bool, typer.Option('--yes', help='跳过危险命令确认')] -DryRunOption = Annotated[bool, typer.Option('--dry-run', help='仅执行预演,不落地实际变更')] diff --git a/shierfeng-fastapi-backend/build/lib/cli/core/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/core/__init__.py deleted file mode 100644 index 17df42c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/core/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from dataclasses import dataclass, field - -from cli.core.app_builder import CliApplicationBuilder, ProjectRuntimeLocator -from cli.core.completion_dispatcher import CompletionDispatcher -from cli.core.context_factory import CliContextFactory, CliRuntimeState -from cli.core.execution import CliExecutionService -from cli.output import OutputRenderer - - -@dataclass -class CoreServiceContainer: - """ - CLI 核心服务容器。 - - 该容器负责统一装配输出渲染器、运行期状态、上下文工厂与执行服务, - 作为控制器、向导和 TUI 等编排层的默认依赖入口。 - - :param output_renderer: 输出渲染器 - :param runtime_state: CLI 运行期状态对象 - """ - - output_renderer: OutputRenderer = field(default_factory=OutputRenderer) - runtime_state: CliRuntimeState = field(default_factory=CliRuntimeState) - context_factory: CliContextFactory = field(init=False) - execution_service: CliExecutionService = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化核心服务依赖图。 - - :return: None - """ - self.context_factory = CliContextFactory( - runtime_state=self.runtime_state, - output_renderer=self.output_renderer, - ) - self.execution_service = CliExecutionService(output_renderer=self.output_renderer) - - -DEFAULT_CORE_SERVICES = CoreServiceContainer() - -__all__ = [ - 'DEFAULT_CORE_SERVICES', - 'CliApplicationBuilder', - 'CliContextFactory', - 'CliExecutionService', - 'CliRuntimeState', - 'CompletionDispatcher', - 'CoreServiceContainer', - 'ProjectRuntimeLocator', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/core/app_builder.py b/shierfeng-fastapi-backend/build/lib/cli/core/app_builder.py deleted file mode 100644 index a6ad2ab..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/core/app_builder.py +++ /dev/null @@ -1,363 +0,0 @@ -import importlib -import sys -from collections.abc import Callable -from dataclasses import dataclass, field -from pathlib import Path -from typing import Annotated, Literal - -import typer - -from cli.output import OutputRenderer -from cli.runtime.base import RUNTIME_ENVIRONMENT - - -@dataclass(frozen=True) -class ProjectRuntimeLocator: - """ - CLI 项目运行时定位器。 - - 该对象负责判断当前目录是否为后端项目根目录、注入 `sys.path`, - 并为配置导入阶段提取最小化的 `argv` 参数。 - """ - - def is_backend_project_dir(self, project_dir: Path) -> bool: - """ - 判断给定目录是否为后端项目根目录。 - - :param project_dir: 待检查目录 - :return: 是否为后端项目根目录 - """ - return RUNTIME_ENVIRONMENT.is_backend_project_dir(project_dir) - - def ensure_backend_dir_on_sys_path(self) -> Path: - """ - 将当前后端项目根目录加入 `sys.path` 首位。 - - :return: 当前后端项目根目录 - :raises typer.Exit: 当前目录不是后端项目根目录时退出命令 - """ - project_dir = Path.cwd().resolve() - if not self.is_backend_project_dir(project_dir): - typer.echo('`shierfeng` 命令需在 `shierfeng-fastapi-backend` 目录下执行', err=True) - raise typer.Exit(code=2) - - project_dir_str = str(project_dir) - if sys.path and sys.path[0] == project_dir_str: - return project_dir - - sys.path.insert(0, project_dir_str) - return project_dir - - def extract_import_argv(self, argv: list[str] | None = None) -> list[str]: - """ - 提取供配置模块导入阶段使用的最小参数集。 - - :param argv: 原始命令行参数列表 - :return: 仅保留程序名和 `--env` 的参数列表 - """ - current_argv = argv or sys.argv - program_name = current_argv[0] if current_argv else 'shierfeng' - import_argv = [program_name] - env_value = '' - current_args = current_argv[1:] - for index, argument in enumerate(current_args): - if argument.startswith('--env='): - env_value = argument.split('=', 1)[1].strip() - break - if argument == '--env' and index + 1 < len(current_args): - env_value = current_args[index + 1].strip() - break - - if env_value: - import_argv.extend(['--env', env_value]) - return import_argv - - -@dataclass(frozen=True) -class CliCommandGroupRegistry: - """ - CLI 命令组注册表。 - - 该注册表集中维护根 CLI 需要挂载的业务命令组模块路径, - 避免命令组声明散落在应用构建器主体中。 - - :param command_modules: 命令组到模块路径的映射 - """ - - command_modules: dict[str, str] - - -@dataclass(frozen=True) -class CliExtensionRegistration: - """ - CLI 扩展入口注册描述。 - - :param module_path: 扩展模块导入路径 - :param attach: 已加载模块挂载到根 CLI 的函数 - """ - - module_path: str - attach: Callable[[typer.Typer, object], None] - - -@dataclass(frozen=True) -class CliExtensionRegistry: - """ - CLI 扩展入口注册表。 - - 该注册表集中维护 completion、wizard、TUI 等扩展入口的模块路径与 - 挂载方式,避免这些规则继续散落在注册器主体中。 - - :param registrations: 扩展注册描述列表 - """ - - registrations: tuple[CliExtensionRegistration, ...] - - -@dataclass(frozen=True) -class CliCommandGroupRegistrar: - """ - CLI 命令组注册器。 - - 该对象负责根据命令组注册表向根 Typer 应用挂载业务命令组。 - - :param command_group_registry: CLI 命令组注册表 - """ - - command_group_registry: CliCommandGroupRegistry - module_loader: 'CliModuleLoader' = field(default_factory=lambda: DEFAULT_CLI_MODULE_LOADER) - - def register(self, cli: typer.Typer) -> None: - """ - 向根应用注册业务命令组。 - - :param cli: 根 Typer 应用 - :return: None - """ - for command_name, module_path in self.command_group_registry.command_modules.items(): - command_module = self.module_loader.load(module_path) - cli.add_typer(command_module.app, name=command_name) - - -class CliExtensionMountSupport: - """ - CLI 扩展入口挂载支持对象。 - - 该对象负责封装不同扩展模块挂载到根 CLI 的差异,避免扩展注册器本体 - 继续堆积 `if/else` 或硬编码模块处理逻辑。 - """ - - @staticmethod - def attach_completion(cli: typer.Typer, module: object) -> None: - """ - 挂载 completion 扩展入口。 - - :param cli: 根 Typer 应用 - :param module: 已加载模块 - :return: None - """ - cli.add_typer(module.COMPLETION_COMMAND_BUILDER.build(cli), name='completion') - - @staticmethod - def attach_wizard(cli: typer.Typer, module: object) -> None: - """ - 挂载 wizard 扩展入口。 - - :param cli: 根 Typer 应用 - :param module: 已加载模块 - :return: None - """ - cli.add_typer(module.WIZARD_COMMAND_BUILDER.build(), name='wizard') - - @staticmethod - def attach_tui(cli: typer.Typer, module: object) -> None: - """ - 挂载 TUI 扩展入口。 - - :param cli: 根 Typer 应用 - :param module: 已加载模块 - :return: None - """ - module.TUI_COMMAND_REGISTRATION.register(cli) - - -@dataclass(frozen=True) -class CliExtensionRegistrar: - """ - CLI 扩展入口注册器。 - - 该对象负责向根 Typer 应用挂载 completion、wizard 与 TUI - 等扩展入口。 - """ - - extension_registry: CliExtensionRegistry - module_loader: 'CliModuleLoader' = field(default_factory=lambda: DEFAULT_CLI_MODULE_LOADER) - - def register(self, cli: typer.Typer) -> None: - """ - 向根应用注册扩展入口。 - - :param cli: 根 Typer 应用 - :return: None - """ - for registration in self.extension_registry.registrations: - extension_module = self.module_loader.load(registration.module_path) - registration.attach(cli, extension_module) - - -@dataclass(frozen=True) -class CliRootOptionInitializer: - """ - CLI 根级选项初始化器。 - - 该对象负责将根级 `--color`、`--icon` 选项映射到共享输出渲染器, - 避免根回调继续内联输出配置逻辑。 - - :param output_renderer: 根命令共享的输出渲染器 - """ - - output_renderer: OutputRenderer - - def initialize(self, *, color: str, icon: str) -> None: - """ - 初始化根级输出选项。 - - :param color: 文本输出颜色模式 - :param icon: 文本输出图标模式 - :return: None - """ - self.output_renderer.set_color_mode(color) - self.output_renderer.set_icon_mode(icon) - - -@dataclass(frozen=True) -class CliRootCallbackRegistrar: - """ - CLI 根回调注册器。 - - 该对象负责把根级 `--color`、`--icon` 选项回调挂载到 Typer 根应用, - 让 `CliApplicationBuilder` 更专注于整体装配流程。 - - :param root_option_initializer: 根级选项初始化器 - """ - - root_option_initializer: CliRootOptionInitializer - - def register(self, cli: typer.Typer) -> None: - """ - 向根应用注册回调。 - - :param cli: 根 Typer 应用 - :return: None - """ - - @cli.callback() - def root_callback( - color: Annotated[ - Literal['auto', 'always', 'never'], - typer.Option('--color', help='文本输出颜色模式'), - ] = 'always', - icon: Annotated[ - Literal['emoji', 'ascii', 'none'], - typer.Option('--icon', help='文本输出图标模式'), - ] = 'emoji', - ) -> None: - """ - 初始化 CLI 根级运行参数。 - - :param color: 文本输出颜色模式 - :param icon: 文本输出图标模式 - :return: None - """ - self.root_option_initializer.initialize(color=color, icon=icon) - - -@dataclass(frozen=True) -class CliModuleLoader: - """ - CLI 模块加载服务。 - - 该对象负责集中执行命令组与扩展入口所需的延迟导入, - 让注册器本身更专注于挂载动作。 - """ - - @staticmethod - def load(module_path: str) -> object: - """ - 按模块路径加载模块对象。 - - :param module_path: 模块导入路径 - :return: 已加载模块 - """ - return importlib.import_module(module_path) - - -@dataclass -class CliApplicationBuilder: - """ - CLI 根应用构建器。 - - :param output_renderer: 根命令共享的输出渲染器 - :param command_group_registrar: CLI 命令组注册器 - :param extension_registrar: CLI 扩展入口注册器 - """ - - output_renderer: OutputRenderer - command_group_registrar: CliCommandGroupRegistrar = field( - default_factory=lambda: CliCommandGroupRegistrar(DEFAULT_COMMAND_GROUP_REGISTRY) - ) - extension_registrar: CliExtensionRegistrar = field( - default_factory=lambda: CliExtensionRegistrar(DEFAULT_CLI_EXTENSION_REGISTRY) - ) - root_option_initializer: CliRootOptionInitializer = field(init=False) - root_callback_registrar: CliRootCallbackRegistrar = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化根应用构建器依赖。 - - :return: None - """ - self.root_option_initializer = CliRootOptionInitializer(self.output_renderer) - self.root_callback_registrar = CliRootCallbackRegistrar(self.root_option_initializer) - - def build(self) -> typer.Typer: - """ - 构建 Typer 根应用并注册全部命令组。 - - :return: Typer 根应用 - """ - cli = typer.Typer( - name='shierfeng', - help='ShiErFeng FastAPI 后端统一命令入口', - no_args_is_help=True, - add_completion=False, - context_settings={'help_option_names': ['-h', '--help']}, - ) - self.root_callback_registrar.register(cli) - self.command_group_registrar.register(cli) - self.extension_registrar.register(cli) - return cli - - -DEFAULT_COMMAND_GROUP_REGISTRY = CliCommandGroupRegistry( - command_modules={ - 'app': 'cli.groups.app', - 'db': 'cli.groups.db', - 'ops': 'cli.groups.ops', - 'cache': 'cli.groups.cache', - 'job': 'cli.groups.job', - 'config': 'cli.groups.config', - 'crypto': 'cli.groups.crypto', - 'gen': 'cli.groups.gen', - 'dev': 'cli.groups.dev', - } -) -DEFAULT_CLI_EXTENSION_REGISTRY = CliExtensionRegistry( - registrations=( - CliExtensionRegistration('cli.completion.commands', CliExtensionMountSupport.attach_completion), - CliExtensionRegistration('cli.wizard.commands', CliExtensionMountSupport.attach_wizard), - CliExtensionRegistration('cli.tui', CliExtensionMountSupport.attach_tui), - ) -) -DEFAULT_CLI_MODULE_LOADER = CliModuleLoader() diff --git a/shierfeng-fastapi-backend/build/lib/cli/core/completion_dispatcher.py b/shierfeng-fastapi-backend/build/lib/cli/core/completion_dispatcher.py deleted file mode 100644 index d09cd54..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/core/completion_dispatcher.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -from dataclasses import dataclass, field - -import typer -from click.shell_completion import shell_complete as click_shell_complete -from typer.completion import shell_complete as typer_shell_complete - -from cli.completion.shells import ensure_custom_completion_classes_registered - - -@dataclass(frozen=True) -class CompletionInstructionSupport: - """ - completion 指令解析支持对象。 - - 该对象负责管理 completion 环境变量名、受支持 shell 列表,以及 - Click / Typer 两种指令格式的判定逻辑。 - - :param complete_env_var: completion 环境变量名 - :param supported_shells: 支持的 shell 名称列表 - """ - - complete_env_var: str = '_SHIERFENG_COMPLETE' - supported_shells: tuple[str, ...] = ('bash', 'zsh', 'fish', 'powershell') - - def is_click_style_instruction(self, instruction: str) -> bool: - """ - 判断 completion 指令是否为 Click 原生格式。 - - Click 原生格式为 `_`,例如 `bash_complete`。 - - :param instruction: completion 指令文本 - :return: 是否为 Click 风格指令 - """ - return any(instruction.startswith(f'{shell}_') for shell in self.supported_shells) - - def is_typer_style_instruction(self, instruction: str) -> bool: - """ - 判断 completion 指令是否为 Typer 兼容格式。 - - Typer 兼容格式为 `_`,例如 `complete_bash`。 - - :param instruction: completion 指令文本 - :return: 是否为 Typer 风格指令 - """ - return any(instruction.endswith(f'_{shell}') for shell in self.supported_shells) - - -@dataclass(frozen=True) -class CompletionInstructionReader: - """ - completion 指令读取器。 - - 该对象负责从当前进程环境中读取并规范化 completion 指令。 - - :param support: completion 指令解析支持对象 - """ - - support: CompletionInstructionSupport - - def read_instruction(self) -> str: - """ - 读取当前进程中的 completion 指令文本。 - - :return: 规范化后的 completion 指令文本 - """ - return os.environ.get(self.support.complete_env_var, '').strip() - - -@dataclass(frozen=True) -class CompletionDispatcher: - """ - shell completion 请求分发器。 - - 该对象负责识别 Click 与 Typer 两种 completion 指令格式, - 并在命中时直接执行补全流程后退出当前进程。 - - :param support: completion 指令解析支持对象 - :param instruction_reader: completion 指令读取器 - """ - - support: CompletionInstructionSupport = field(default_factory=CompletionInstructionSupport) - instruction_reader: CompletionInstructionReader = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化 completion 指令读取器。 - - :return: None - """ - object.__setattr__(self, 'instruction_reader', CompletionInstructionReader(self.support)) - - def dispatch(self, cli: typer.Typer) -> None: - """ - 处理 shell completion 请求并在命中时直接退出。 - - 这里同时兼容 Click 与 Typer 两种 completion 指令格式,避免 - 生成脚本格式和运行时解析格式不一致时出现 `Shell complete not supported.`。 - - :param cli: Typer 根应用 - :return: None - """ - ensure_custom_completion_classes_registered() - instruction = self.instruction_reader.read_instruction() - if not instruction: - return - - click_command = typer.main.get_command(cli) - if self.support.is_click_style_instruction(instruction): - raise SystemExit( - click_shell_complete(click_command, {}, 'shierfeng', self.support.complete_env_var, instruction) - ) - if self.support.is_typer_style_instruction(instruction): - raise SystemExit( - typer_shell_complete(click_command, {}, 'shierfeng', self.support.complete_env_var, instruction) - ) - - raise SystemExit(1) - - -DEFAULT_COMPLETION_INSTRUCTION_SUPPORT = CompletionInstructionSupport() diff --git a/shierfeng-fastapi-backend/build/lib/cli/core/context_factory.py b/shierfeng-fastapi-backend/build/lib/cli/core/context_factory.py deleted file mode 100644 index 23adf62..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/core/context_factory.py +++ /dev/null @@ -1,239 +0,0 @@ -import logging -from dataclasses import dataclass, field -from importlib import import_module -from typing import Any - -from cli.context import CLI_CONTEXT_BUILDER, CliContext, CliContextBuilder -from cli.guards import ( - DEFAULT_DANGEROUS_COMMAND_GUARD, - DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY, - DangerousCommandGuardService, - DangerousCommandRuleRegistry, -) -from cli.output import OutputRenderer - - -@dataclass -class CliRuntimeState: - """ - CLI 运行期状态。 - - :param logs_suppressed: 当前进程日志是否已切换为静默模式 - """ - - logs_suppressed: bool = False - sqlalchemy_logs_suppressed: bool = False - - def get_logger(self) -> Any: - """ - 获取 CLI 使用的日志对象。 - - :return: 日志对象 - """ - return import_module('utils.log_util').logger - - def suppress_logs(self) -> None: - """ - 关闭 CLI 进程中的终端日志输出。 - - :return: None - """ - if self.logs_suppressed: - return - self.get_logger().remove() - self.logs_suppressed = True - - def suppress_sqlalchemy_logs(self) -> None: - """ - 关闭 CLI 进程中的 SQLAlchemy 终端 SQL 日志输出。 - - 该逻辑同时处理两类来源: - - 1. 将 `config.env.DataBaseConfig.db_echo` 强制关闭,避免后续新建 Engine 时 - 继续打开 SQLAlchemy echo。 - 2. 将已知 SQLAlchemy logger 级别提升到 WARNING,避免已有 logger 配置把 - `INFO sqlalchemy.engine.Engine ...` 继续打到标准输出。 - - :return: None - """ - if self.sqlalchemy_logs_suppressed: - return - env_module = import_module('config.env') - database_config = getattr(env_module, 'DataBaseConfig', None) - if database_config is not None and hasattr(database_config, 'db_echo'): - database_config.db_echo = False - for logger_name in ( - 'sqlalchemy', - 'sqlalchemy.engine', - 'sqlalchemy.engine.Engine', - 'sqlalchemy.pool', - ): - logging.getLogger(logger_name).setLevel(logging.WARNING) - self.sqlalchemy_logs_suppressed = True - - -@dataclass -class CliLogPolicy: - """ - CLI 日志策略服务。 - - 该对象负责在上下文构建前应用统一的日志输出策略, - 将“是否静默日志”这一策略从上下文工厂主体中拆出。 - - :param runtime_state: CLI 运行期状态对象 - """ - - runtime_state: CliRuntimeState - - def prepare_regular_command(self) -> None: - """ - 为普通 CLI 命令应用默认日志策略。 - - :return: None - """ - self.runtime_state.suppress_logs() - self.runtime_state.suppress_sqlalchemy_logs() - - -@dataclass -class DangerousCommandContextSupport: - """ - 危险命令上下文支持服务。 - - 该对象负责危险命令规则查询、保护执行与拒绝结果收口, - 让上下文工厂本体只保留装配职责。 - - :param dangerous_command_rule_registry: 危险命令规则注册表 - :param dangerous_command_guard_service: 危险命令保护执行服务 - :param output_renderer: 输出渲染器 - """ - - dangerous_command_rule_registry: DangerousCommandRuleRegistry - dangerous_command_guard_service: DangerousCommandGuardService - output_renderer: OutputRenderer - - def guard_context(self, ctx: CliContext, *, command_name: str) -> CliContext: - """ - 对危险命令上下文执行统一保护。 - - :param ctx: 已构建的 CLI 上下文 - :param command_name: 命令唯一标识 - :return: 通过保护后的 CLI 上下文 - """ - rule = self.dangerous_command_rule_registry.require_rule(command_name) - guard_result = self.dangerous_command_guard_service.guard(ctx, rule=rule) - if guard_result is not None: - self.output_renderer.complete_command(guard_result, ctx) - return ctx - - -@dataclass -class CliContextFactory: - """ - 统一构建 CLI 命令上下文。 - - :param runtime_state: CLI 运行期状态对象 - :param output_renderer: 输出渲染器 - :param cli_context_builder: CLI 上下文构建器 - :param dangerous_command_rule_registry: 危险命令规则注册表 - :param dangerous_command_guard_service: 危险命令保护执行服务 - """ - - runtime_state: CliRuntimeState = field(default_factory=CliRuntimeState) - output_renderer: OutputRenderer = field(default_factory=OutputRenderer) - cli_context_builder: CliContextBuilder = field(default_factory=lambda: CLI_CONTEXT_BUILDER) - dangerous_command_rule_registry: DangerousCommandRuleRegistry = field( - default_factory=lambda: DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY - ) - dangerous_command_guard_service: DangerousCommandGuardService = field( - default_factory=lambda: DEFAULT_DANGEROUS_COMMAND_GUARD - ) - _log_policy: CliLogPolicy | None = field(default=None, init=False, repr=False) - _dangerous_command_support: DangerousCommandContextSupport | None = field( - default=None, - init=False, - repr=False, - ) - - def get_log_policy(self) -> CliLogPolicy: - """ - 获取当前上下文工厂使用的日志策略服务。 - - :return: CLI 日志策略服务 - """ - if self._log_policy is None: - self._log_policy = CliLogPolicy(runtime_state=self.runtime_state) - return self._log_policy - - def get_dangerous_command_support(self) -> DangerousCommandContextSupport: - """ - 获取当前上下文工厂使用的危险命令上下文支持服务。 - - :return: 危险命令上下文支持服务 - """ - if self._dangerous_command_support is None: - self._dangerous_command_support = DangerousCommandContextSupport( - dangerous_command_rule_registry=self.dangerous_command_rule_registry, - dangerous_command_guard_service=self.dangerous_command_guard_service, - output_renderer=self.output_renderer, - ) - return self._dangerous_command_support - - def build_regular( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> CliContext: - """ - 构建普通命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :return: CLI 上下文 - """ - self.get_log_policy().prepare_regular_command() - return self.cli_context_builder.build(env, output, allow_prod, yes, dry_run) - - def build_readonly( - self, - env: str, - output: str, - ) -> CliContext: - """ - 构建只读命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_regular(env, output, False, False, False) - - def build_dangerous( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - command_name: str, - ) -> CliContext: - """ - 构建危险命令上下文并执行统一保护。 - - :param env: 运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param command_name: 命令唯一标识 - :return: CLI 上下文 - """ - ctx = self.build_regular(env, output, allow_prod, yes, dry_run) - return self.get_dangerous_command_support().guard_context(ctx, command_name=command_name) diff --git a/shierfeng-fastapi-backend/build/lib/cli/core/execution.py b/shierfeng-fastapi-backend/build/lib/cli/core/execution.py deleted file mode 100644 index 2afd47c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/core/execution.py +++ /dev/null @@ -1,137 +0,0 @@ -import asyncio -from collections.abc import Callable -from dataclasses import dataclass, field -from typing import Any - -from cli.context import CliContext -from cli.exit_codes import RUNTIME_ERROR -from cli.output import CommandResult, OutputRenderer - - -@dataclass -class CliExecutionService: - """ - CLI 执行与结果收口服务。 - - :param output_renderer: 输出渲染器 - """ - - output_renderer: OutputRenderer = field(default_factory=OutputRenderer) - - def run_async(self, coroutine: Any) -> Any: - """ - 执行异步协程并返回结果。 - - :param coroutine: 待执行协程 - :return: 协程执行结果 - """ - return asyncio.run(coroutine) - - def complete_payload( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - default_exit_code: int = 0, - ) -> None: - """ - 输出标准字典负载并结束命令。 - - :param ctx: CLI 上下文 - :param payload: 标准结果字典 - :param default_exit_code: 默认退出码 - :return: None - """ - self.output_renderer.complete_command(self.build_result(payload, default_exit_code=default_exit_code), ctx) - - def complete_result( - self, - ctx: CliContext, - result: CommandResult, - ) -> None: - """ - 输出命令结果对象并结束命令。 - - :param ctx: CLI 上下文 - :param result: 命令执行结果对象 - :return: None - """ - self.output_renderer.complete_command(result, ctx) - - def complete_payload_with_text( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - text_builder: Callable[[dict[str, Any]], Any], - default_exit_code: int = 0, - text_condition: Callable[[dict[str, Any]], bool] | None = None, - ) -> None: - """ - 按输出格式收口标准负载,必要时先转换为文本结果。 - - :param ctx: CLI 上下文 - :param payload: 标准结果字典 - :param text_builder: 文本结果构建函数 - :param default_exit_code: 默认退出码 - :param text_condition: 文本模式下是否应用构建函数的判定函数 - :return: None - """ - if ctx.output != 'text': - self.complete_payload(ctx, payload, default_exit_code=default_exit_code) - return - - result = self.build_result(payload, default_exit_code=default_exit_code) - should_build_text = True - if callable(text_condition) and isinstance(result.data, dict): - should_build_text = bool(text_condition(result.data)) - if should_build_text and isinstance(result.data, dict): - result.data = text_builder(result.data) - self.complete_result(ctx, result) - - def complete_payload_result( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - text_builder: Callable[[dict[str, Any]], Any] | None = None, - default_exit_code: int = 0, - text_condition: Callable[[dict[str, Any]], bool] | None = None, - ) -> None: - """ - 统一收口标准负载,按输出格式决定是否渲染为文本结果。 - - :param ctx: CLI 上下文 - :param payload: 标准结果字典 - :param text_builder: 文本结果构建函数 - :param default_exit_code: 默认退出码 - :param text_condition: 文本模式下是否应用构建函数的判定函数 - :return: None - """ - if text_builder is None: - self.complete_payload(ctx, payload, default_exit_code=default_exit_code) - return - self.complete_payload_with_text( - ctx, - payload, - text_builder=text_builder, - default_exit_code=default_exit_code, - text_condition=text_condition, - ) - - @staticmethod - def build_result(payload: dict[str, Any], *, default_exit_code: int = 0) -> CommandResult: - """ - 将标准字典负载转换为命令结果对象。 - - :param payload: 标准结果字典 - :param default_exit_code: 默认退出码;当 payload 未显式提供 exit_code 且 - 该值仍为 0 时,失败结果会统一回退到 `RUNTIME_ERROR` - :return: 命令结果对象 - """ - result_payload = dict(payload) - exit_code = result_payload.pop('exit_code', None) - if exit_code is None: - is_ok = bool(result_payload.get('ok', True)) - exit_code = default_exit_code if is_ok or default_exit_code != 0 else RUNTIME_ERROR - return CommandResult(data=result_payload, exit_code=exit_code) diff --git a/shierfeng-fastapi-backend/build/lib/cli/exit_codes.py b/shierfeng-fastapi-backend/build/lib/cli/exit_codes.py deleted file mode 100644 index ead6bb0..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/exit_codes.py +++ /dev/null @@ -1,8 +0,0 @@ -SUCCESS = 0 -ARGUMENT_ERROR = 2 -DEPENDENCY_ERROR = 10 -DATABASE_ERROR = 20 -REDIS_ERROR = 21 -SCHEDULER_ERROR = 22 -GUARD_REJECTED = 30 -RUNTIME_ERROR = 50 diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/app/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/app/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/app/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/app/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/app/command.py deleted file mode 100644 index 40bcea4..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/app/command.py +++ /dev/null @@ -1,102 +0,0 @@ -from typing import Annotated, Literal - -import typer - -from cli.context import EnvOption, OutputOption - -from .controller import AppCommandController - -app = typer.Typer( - help='应用相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_APP_COMMAND_CONTROLLER = AppCommandController() - - -@app.command('run', help='启动当前 FastAPI 应用') -def run_app( - env: EnvOption = 'dev', -) -> None: - """ - 启动当前 FastAPI 应用。 - - :param env: 当前命令运行环境 - :return: None - """ - _APP_COMMAND_CONTROLLER.run_app(env) - - -@app.command('doctor', help='执行应用启动前检查') -def doctor( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 执行应用启动前检查。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _APP_COMMAND_CONTROLLER.doctor(env, output) - - -@app.command('config', help='查看当前应用配置快照') -def app_config( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 输出当前应用配置快照。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _APP_COMMAND_CONTROLLER.show_config(env, output) - - -@app.command('env', help='查看当前 CLI 解析到的应用环境信息') -def app_env( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 输出当前 CLI 解析到的应用环境信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _APP_COMMAND_CONTROLLER.show_env(env, output) - - -@app.command('routes', help='查看当前应用注册路由清单') -def routes( - env: EnvOption = 'dev', - output: OutputOption = 'text', - path_prefix: Annotated[str, typer.Option('--path-prefix', help='按路由前缀过滤')] = '', - method: Annotated[str, typer.Option('--method', help='按请求方法过滤,如 GET、POST')] = '', - group_by: Annotated[Literal['none', 'tag'], typer.Option('--group-by', help='路由分组方式')] = 'none', - include_hidden: Annotated[bool, typer.Option('--include-hidden', help='包含未出现在 OpenAPI 中的路由')] = False, -) -> None: - """ - 输出当前应用注册路由清单。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param path_prefix: 路由前缀过滤条件 - :param method: 请求方法过滤条件 - :param group_by: 路由分组方式 - :param include_hidden: 是否包含隐藏路由 - :return: None - """ - _APP_COMMAND_CONTROLLER.show_routes( - env, - output, - path_prefix=path_prefix, - method=method, - group_by=group_by, - include_hidden=include_hidden, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/app/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/app/controller.py deleted file mode 100644 index 2e4a4e8..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/app/controller.py +++ /dev/null @@ -1,236 +0,0 @@ -from collections import defaultdict -from typing import Any - -from fastapi.routing import APIRoute - -from cli.bootstrap import APP_BOOTSTRAP, AppBootstrapService -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS -from cli.runtime.app import APP_RUNTIME, AppRuntimeService -from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService -from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService -from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService - -from .presenter import AppCommandPresenter - - -class AppCommandController: - """ - 应用命令控制器。 - - 该控制器负责组织 `app` 命令组的上下文准备、runtime 调用、 - payload 构建,以及基于输出格式选择 presenter 或直接返回 JSON。 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: AppCommandPresenter | None = None, - runtime_service: AppRuntimeService | None = None, - database_runtime: DatabaseRuntimeService | None = None, - operations_runtime: OperationsRuntimeService | None = None, - crypto_runtime: CryptoRuntimeService | None = None, - bootstrap_service: AppBootstrapService | None = None, - ) -> None: - """ - 初始化应用命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 应用命令文本渲染器 - :param runtime_service: 应用运行时服务 - :param database_runtime: 数据库运行时服务 - :param operations_runtime: 运维运行时服务 - :param crypto_runtime: 传输加密运行时服务 - :param bootstrap_service: 应用引导服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or AppCommandPresenter() - self.runtime_service = runtime_service or APP_RUNTIME - self.database_runtime = database_runtime or DATABASE_RUNTIME - self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME - self.crypto_runtime = crypto_runtime or CRYPTO_RUNTIME - self.bootstrap_service = bootstrap_service or APP_BOOTSTRAP - - def run_app(self, env: str) -> None: - """ - 启动当前 FastAPI 应用。 - - :param env: 当前命令运行环境 - :return: None - """ - self.bootstrap_service.exec_app_run_command(env) - - def doctor(self, env: str, output: str) -> None: - """ - 执行应用启动前检查。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - db_status = self.execution_service.run_async(self.database_runtime.ping_database()) - redis_status = self.execution_service.run_async(self.operations_runtime.ping_redis()) - crypto_status = self.crypto_runtime.validate_crypto_config() - payload = { - 'env': ctx.env, - 'database': db_status, - 'redis': redis_status, - 'crypto': crypto_status, - } - payload['ok'] = all(item.get('ok', False) for item in (db_status, redis_status, crypto_status)) - exit_code = SUCCESS if payload['ok'] else DEPENDENCY_ERROR - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_doctor_text, - default_exit_code=exit_code, - ) - - def show_config(self, env: str, output: str) -> None: - """ - 输出当前应用配置快照。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload_result( - ctx, - {'ok': True, 'env': ctx.env, 'config': self.runtime_service.get_app_config_snapshot()}, - text_builder=self.presenter.build_app_config_text, - default_exit_code=SUCCESS, - ) - - def show_env(self, env: str, output: str) -> None: - """ - 输出当前 CLI 解析到的应用环境信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload_result( - ctx, - {'ok': True, 'env': ctx.env, 'runtime': self.runtime_service.get_app_env_snapshot()}, - text_builder=self.presenter.build_app_env_text, - default_exit_code=SUCCESS, - ) - - def show_routes( - self, - env: str, - output: str, - *, - path_prefix: str, - method: str, - group_by: str, - include_hidden: bool, - ) -> None: - """ - 输出当前应用注册路由清单。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param path_prefix: 路由前缀过滤条件 - :param method: 请求方法过滤条件 - :param group_by: 路由分组方式 - :param include_hidden: 是否包含隐藏路由 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - routes_payload = self._serialize_routes( - self.runtime_service.build_app_instance(), - path_prefix=path_prefix, - method=method, - include_hidden=include_hidden, - ) - grouped_routes = self._group_routes_by_tag(routes_payload) if group_by == 'tag' else None - payload = { - 'ok': True, - 'env': ctx.env, - 'count': len(routes_payload), - 'filters': { - 'pathPrefix': path_prefix, - 'method': method.upper().strip(), - 'groupBy': group_by, - 'includeHidden': include_hidden, - }, - 'routes': routes_payload, - 'groupedRoutes': grouped_routes, - } - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_routes_text, - default_exit_code=SUCCESS, - text_condition=lambda data: data.get('ok', False), - ) - - @staticmethod - def _serialize_routes( - app_instance: Any, - *, - path_prefix: str = '', - method: str = '', - include_hidden: bool = False, - ) -> list[dict[str, Any]]: - """ - 序列化 FastAPI 路由信息。 - - :param app_instance: FastAPI 应用实例 - :param path_prefix: 路径前缀过滤条件 - :param method: 请求方法过滤条件 - :param include_hidden: 是否包含未出现在 OpenAPI 中的路由 - :return: 序列化后的路由列表 - """ - normalized_method = method.upper().strip() - routes = [] - for route in app_instance.routes: - if not isinstance(route, APIRoute): - continue - if not include_hidden and not route.include_in_schema: - continue - if path_prefix and not route.path.startswith(path_prefix): - continue - route_methods = sorted(item for item in route.methods if item not in {'HEAD', 'OPTIONS'}) - if normalized_method and normalized_method not in route_methods: - continue - routes.append( - { - 'path': route.path, - 'methods': route_methods, - 'name': route.name, - 'summary': route.summary or '', - 'operationId': route.operation_id or '', - 'tags': route.tags or [], - 'includeInSchema': route.include_in_schema, - } - ) - return sorted(routes, key=lambda item: (item['path'], ','.join(item['methods']))) - - @staticmethod - def _group_routes_by_tag(routes_payload: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]: - """ - 按标签对路由信息分组。 - - :param routes_payload: 原始路由列表 - :return: 按标签分组后的路由映射 - """ - grouped_routes: dict[str, list[dict[str, Any]]] = defaultdict(list) - for route in routes_payload: - tags = route.get('tags') or ['__untagged__'] - for tag in tags: - grouped_routes[tag].append(route) - return dict(sorted(grouped_routes.items(), key=lambda item: item[0])) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/app/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/app/presenter.py deleted file mode 100644 index 7a6cb79..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/app/presenter.py +++ /dev/null @@ -1,210 +0,0 @@ -from typing import Any - -from cli.utils import SHELL_TEXT_FORMATTER - - -class AppCommandPresenter: - """ - 应用命令文本渲染器。 - - 该渲染器负责将 `app` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_routes_text(self, payload: dict[str, Any]) -> str: - """ - 将路由列表结果渲染为文本摘要。 - - :param payload: 路由列表结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'count: {payload.get("count", 0)}', - ] - filters = payload.get('filters') - if isinstance(filters, dict): - lines.extend(self._build_route_filter_lines(filters)) - - grouped_routes = payload.get('groupedRoutes') - if filters and filters.get('groupBy') == 'tag' and isinstance(grouped_routes, dict): - lines.extend(self._build_grouped_routes_text(grouped_routes)) - return '\n'.join(lines) - - routes = payload.get('routes') - if not isinstance(routes, list) or not routes: - lines.append('routes: none') - return '\n'.join(lines) - - lines.append('routes:') - for route in routes: - if isinstance(route, dict): - lines.extend([f' {line}' for line in self._build_route_item_lines(route)]) - return '\n'.join(lines) - - def build_app_config_text(self, payload: dict[str, Any]) -> str: - """ - 将应用配置快照渲染为文本摘要。 - - :param payload: 应用配置结果字典 - :return: 文本摘要 - """ - config = payload.get('config') - if not isinstance(config, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'config: none', - ] - ) - - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'application:', - f' name: {config.get("name", "-")}', - f' host: {config.get("host", "-")}:{config.get("port", "-")}', - f' root_path: {config.get("rootPath", "-") or "/"}', - f' reload: {str(config.get("reload", False)).lower()}', - f' workers: {config.get("workers", "-")}', - f' disable_swagger: {str(config.get("disableSwagger", False)).lower()}', - f' disable_redoc: {str(config.get("disableRedoc", False)).lower()}', - 'database:', - f' type: {config.get("dbType", "-")}', - f' host: {config.get("dbHost", "-")}:{config.get("dbPort", "-")}', - f' name: {config.get("dbDatabase", "-")}', - 'redis:', - f' host: {config.get("redisHost", "-")}:{config.get("redisPort", "-")}', - 'logging:', - f' level: {config.get("logLevel", "-")}', - 'transport_crypto:', - f' enabled: {str(config.get("transportCryptoEnabled", False)).lower()}', - f' mode: {config.get("transportCryptoMode", "-")}', - ] - return '\n'.join(lines) - - def build_app_env_text(self, payload: dict[str, Any]) -> str: - """ - 将应用环境解析结果渲染为文本摘要。 - - :param payload: 应用环境结果字典 - :return: 文本摘要 - """ - runtime = payload.get('runtime') - if not isinstance(runtime, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'runtime: none', - ] - ) - - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'runtime:', - f' cli_env: {runtime.get("cliEnv", "-")}', - f' config_env: {runtime.get("configEnv", "-")}', - f' app_env: {runtime.get("appEnv", "-")}', - f' env_file: {runtime.get("envFile", "-")}', - f' env_file_exists: {str(runtime.get("envFileExists", False)).lower()}', - f' backend_dir: {runtime.get("backendDir", "-")}', - f' python_executable: {runtime.get("pythonExecutable", "-")}', - ] - ) - - def build_doctor_text(self, payload: dict[str, Any]) -> str: - """ - 将应用启动前检查结果渲染为文本摘要。 - - :param payload: 启动前检查结果字典 - :return: 文本摘要 - """ - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'checks:', - self._build_check_status_line('database', payload.get('database')), - self._build_check_status_line('redis', payload.get('redis')), - self._build_check_status_line('crypto', payload.get('crypto')), - ] - ) - - @staticmethod - def _build_route_filter_lines(filters: dict[str, object]) -> list[str]: - """ - 构建路由过滤条件文本行。 - - :param filters: 过滤条件字典 - :return: 过滤条件文本行列表 - """ - active_filters = [] - for key, value in filters.items(): - if value in (None, '', False, 'none'): - continue - active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}') - if not active_filters: - return ['filters: none'] - return ['filters:', *[f' - {item}' for item in active_filters]] - - @staticmethod - def _build_route_item_lines(route_item: dict[str, Any]) -> list[str]: - """ - 构建单条路由记录的文本行。 - - :param route_item: 单条路由记录 - :return: 文本行列表 - """ - methods = ','.join(route_item.get('methods', [])) or '-' - path = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('path', ''), 72) - tags = ','.join(route_item.get('tags', [])) or '-' - summary = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('summary', ''), 60) - name = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('name', ''), 40) - return [ - f'- [{methods}] {path}', - f' name: {name or "-"} | tags: {tags}', - f' summary: {summary or "-"}', - ] - - def _build_grouped_routes_text(self, grouped_routes: dict[str, Any]) -> list[str]: - """ - 将按标签分组后的路由结果渲染为文本行。 - - :param grouped_routes: 按标签分组后的路由映射 - :return: 文本行列表 - """ - if not grouped_routes: - return ['groups: none'] - - lines = [f'groups: {len(grouped_routes)}'] - for tag, routes in grouped_routes.items(): - route_items = routes if isinstance(routes, list) else [] - lines.append(f' {tag}: {len(route_items)}') - for route in route_items: - if isinstance(route, dict): - lines.extend([f' {line}' for line in self._build_route_item_lines(route)]) - return lines - - @staticmethod - def _build_check_status_line(name: str, status_payload: dict[str, Any] | None) -> str: - """ - 构建单项检查结果摘要行。 - - :param name: 检查项名称 - :param status_payload: 检查结果字典 - :return: 摘要行文本 - """ - if not isinstance(status_payload, dict): - return f' {name}: unknown' - - ok = str(status_payload.get('ok', False)).lower() - message = status_payload.get('message', '-') or '-' - error = status_payload.get('error') - if error: - return f' {name}: {ok} | {message} | error: {SHELL_TEXT_FORMATTER.truncate_text(error, 120)}' - return f' {name}: {ok} | {message}' diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/cache/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/cache/command.py deleted file mode 100644 index 6f803df..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/command.py +++ /dev/null @@ -1,163 +0,0 @@ -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY -from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption - -from .controller import CacheCommandController - -app = typer.Typer( - help='缓存相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_CACHE_COMMAND_CONTROLLER = CacheCommandController() - - -@app.command('stats', help='查看缓存统计信息') -def stats( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看缓存统计信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.stats(env, output) - - -@app.command('keys', help='查看指定缓存名称下的键名列表') -def keys( - cache_name: str = typer.Argument( - ..., - help='缓存名称', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看指定缓存名称下的键名列表。 - - :param cache_name: 缓存名称 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.keys(cache_name, env, output) - - -@app.command('get', help='查看指定缓存内容') -def get( - cache_name: str = typer.Argument( - ..., - help='缓存名称', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names, - ), - cache_key: str = typer.Argument( - ..., - help='缓存键名', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_keys, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看指定缓存内容。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.get(cache_name, cache_key, env, output) - - -@app.command('clear', help='执行缓存清理') -def clear( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - cache_name: str = typer.Option( - '', - '--cache-name', - help='按缓存名称前缀清理', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names, - ), - cache_key: str = typer.Option('', '--cache-key', help='按缓存键名模糊清理'), - clear_all: bool = typer.Option(False, '--all', help='清理全部缓存并重建系统基础缓存'), -) -> None: - """ - 清理缓存。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param cache_name: 缓存名称前缀 - :param cache_key: 缓存键名 - :param clear_all: 是否清理全部缓存 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.clear( - env, - output, - allow_prod, - yes, - dry_run, - cache_name=cache_name, - cache_key=cache_key, - clear_all=clear_all, - ) - - -@app.command('ttl', help='查看指定缓存键的剩余过期时间') -def ttl( - cache_name: str = typer.Argument( - ..., - help='缓存名称', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names, - ), - cache_key: str = typer.Argument( - ..., - help='缓存键名', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_keys, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看指定缓存键的剩余过期时间。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.ttl(cache_name, cache_key, env, output) - - -@app.command('warmup', help='执行系统缓存预热') -def warmup( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 执行系统缓存预热。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _CACHE_COMMAND_CONTROLLER.warmup(env, output, allow_prod, yes) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/cache/controller.py deleted file mode 100644 index 92fd656..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/controller.py +++ /dev/null @@ -1,183 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.cache import CACHE_RUNTIME, CacheRuntimeService - -from .presenter import CacheCommandPresenter - - -class CacheCommandController: - """ - 缓存命令控制器。 - - 该控制器负责组织 `cache` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 缓存命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: CacheCommandPresenter | None = None, - runtime_service: CacheRuntimeService | None = None, - ) -> None: - """ - 初始化缓存命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 缓存命令文本渲染器 - :param runtime_service: 缓存运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or CacheCommandPresenter() - self.runtime_service = runtime_service or CACHE_RUNTIME - - def stats(self, env: str, output: str) -> None: - """ - 查看缓存统计信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.get_cache_stats()) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_cache_stats_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def keys(self, cache_name: str, env: str, output: str) -> None: - """ - 查看指定缓存名称下的键名列表。 - - :param cache_name: 缓存名称 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.list_cache_keys(cache_name)) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_cache_keys_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def get(self, cache_name: str, cache_key: str, env: str, output: str) -> None: - """ - 查看指定缓存内容。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.get_cache_value(cache_name, cache_key)) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_cache_value_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def clear( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - cache_name: str, - cache_key: str, - clear_all: bool, - ) -> None: - """ - 清理缓存。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param cache_name: 缓存名称前缀 - :param cache_key: 缓存键名 - :param clear_all: 是否清理全部缓存 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='cache clear', - ) - self.execution_service.complete_payload( - ctx, - self.execution_service.run_async( - self.runtime_service.clear_cache( - cache_name=cache_name, - cache_key=cache_key, - clear_all=clear_all, - dry_run=dry_run, - ) - ), - ) - - def ttl(self, cache_name: str, cache_key: str, env: str, output: str) -> None: - """ - 查看指定缓存键的剩余过期时间。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.get_cache_ttl(cache_name, cache_key)) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_cache_ttl_text, - text_condition=lambda result_data: 'error' not in result_data, - ) - - def warmup(self, env: str, output: str, allow_prod: bool, yes: bool) -> None: - """ - 执行系统缓存预热。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - False, - command_name='cache warmup', - ) - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.runtime_service.warmup_cache()) - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/cache/presenter.py deleted file mode 100644 index 6b96052..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/cache/presenter.py +++ /dev/null @@ -1,150 +0,0 @@ -from cli.utils import SHELL_TEXT_FORMATTER - - -class CacheCommandPresenter: - """ - 缓存命令文本渲染器。 - - 该渲染器负责将 `cache` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_cache_stats_text(self, payload: dict[str, object]) -> str: - """ - 将缓存统计结果渲染为文本摘要。 - - :param payload: 缓存统计结果 - :return: 文本摘要 - """ - info = payload.get('info') - command_stats = payload.get('commandStats') - cache_names = payload.get('cacheNames') - - redis_version = '-' - connected_clients = '-' - used_memory = '-' - uptime_seconds = '-' - keyspace_hits = '-' - keyspace_misses = '-' - if isinstance(info, dict): - redis_version = info.get('redis_version', '-') - connected_clients = info.get('connected_clients', '-') - used_memory = info.get('used_memory_human', info.get('used_memory', '-')) - uptime_seconds = info.get('uptime_in_seconds', '-') - keyspace_hits = info.get('keyspace_hits', '-') - keyspace_misses = info.get('keyspace_misses', '-') - - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'db_size: {payload.get("dbSize", 0)}', - 'redis:', - f' version: {redis_version}', - f' connected_clients: {connected_clients}', - f' used_memory: {used_memory}', - f' uptime_seconds: {uptime_seconds}', - f' keyspace_hits: {keyspace_hits}', - f' keyspace_misses: {keyspace_misses}', - ] - - if isinstance(command_stats, list) and command_stats: - lines.append('command_stats_top10:') - lines.extend( - f' - {item.get("name", "-")}: {item.get("value", 0)}' - for item in command_stats[:10] - if isinstance(item, dict) - ) - else: - lines.append('command_stats_top10: none') - - if isinstance(cache_names, list) and cache_names: - lines.append(f'cache_names: {len(cache_names)}') - lines.append('cache_name_samples:') - lines.extend( - f' - {item.get("cacheName", "-")}: {self._build_cache_name_remark(item.get("remark", ""))}' - for item in cache_names[:10] - if isinstance(item, dict) - ) - else: - lines.append('cache_names: 0') - - return '\n'.join(lines) - - def build_cache_keys_text(self, payload: dict[str, object]) -> str: - """ - 将缓存键列表结果渲染为文本摘要。 - - :param payload: 缓存键列表结果 - :return: 文本摘要 - """ - keys = payload.get('keys') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'cache_name: {payload.get("cacheName", "")}', - f'count: {payload.get("count", 0)}', - ] - if not isinstance(keys, list) or not keys: - lines.append('keys: none') - return '\n'.join(lines) - - lines.append('keys:') - lines.extend(f' - {key}' for key in keys) - return '\n'.join(lines) - - def build_cache_value_text(self, payload: dict[str, object]) -> str: - """ - 将缓存值读取结果渲染为文本摘要。 - - :param payload: 缓存值读取结果字典 - :return: 文本摘要 - """ - cache_value = '' if payload.get('cacheValue') is None else str(payload.get('cacheValue')) - cache_value_lines = cache_value.splitlines() if cache_value else [] - if not cache_value_lines: - rendered_value_lines = [' -'] - elif len(cache_value_lines) == 1: - rendered_value_lines = [f' {SHELL_TEXT_FORMATTER.truncate_text(cache_value_lines[0], 200)}'] - else: - rendered_value_lines = [' |', *[f' {line}' for line in cache_value_lines]] - - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'cache_name: {payload.get("cacheName", "")}', - f'cache_key: {payload.get("cacheKey", "")}', - f'full_cache_key: {payload.get("fullCacheKey", "")}', - 'cache_value:', - *rendered_value_lines, - ] - ) - - @staticmethod - def build_cache_ttl_text(payload: dict[str, object]) -> str: - """ - 将缓存 TTL 结果渲染为文本摘要。 - - :param payload: 缓存 TTL 结果字典 - :return: 文本摘要 - """ - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'cache_name: {payload.get("cacheName", "")}', - f'cache_key: {payload.get("cacheKey", "")}', - f'full_cache_key: {payload.get("fullCacheKey", "")}', - f'message: {payload.get("message", "-")}', - f'ttl_seconds: {payload.get("ttlSeconds", "-")}', - f'persistent: {str(payload.get("persistent", False)).lower()}', - f'expires: {str(payload.get("expires", False)).lower()}', - ] - ) - - @staticmethod - def _build_cache_name_remark(remark: object) -> str: - """ - 规范化缓存名称备注文本。 - - :param remark: 原始备注 - :return: 规范化后的备注 - """ - text = '' if remark is None else str(remark).strip() - return text or '-' diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/config/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/config/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/config/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/config/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/config/command.py deleted file mode 100644 index ed0b695..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/config/command.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Annotated, Literal - -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY -from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption - -from .controller import ConfigCommandController - -app = typer.Typer( - help='参数配置相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_CONFIG_COMMAND_CONTROLLER = ConfigCommandController() - - -@app.command('list', help='查看参数配置列表') -def list_command( - env: EnvOption = 'dev', - output: OutputOption = 'text', - config_name: Annotated[str, typer.Option('--config-name', help='按参数名称过滤')] = '', - config_key: Annotated[ - str, - typer.Option( - '--config-key', - help='按参数键名过滤', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys, - ), - ] = '', - config_type: Annotated[Literal['Y', 'N'] | None, typer.Option('--config-type', help='按系统内置标记过滤')] = None, - begin_date: Annotated[str, typer.Option('--begin-date', help='按创建时间开始日期过滤,格式 YYYY-MM-DD')] = '', - end_date: Annotated[str, typer.Option('--end-date', help='按创建时间结束日期过滤,格式 YYYY-MM-DD')] = '', - paged: Annotated[bool, typer.Option('--paged/--no-paged', help='是否启用分页结果')] = False, - page_num: Annotated[int, typer.Option('--page-num', min=1, help='分页页码')] = 1, - page_size: Annotated[int, typer.Option('--page-size', min=1, help='分页每页数量')] = 20, -) -> None: - """ - 查看参数配置列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param config_name: 参数名称过滤条件 - :param config_key: 参数键名过滤条件 - :param config_type: 参数类型过滤条件 - :param begin_date: 查询开始日期 - :param end_date: 查询结束日期 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - _CONFIG_COMMAND_CONTROLLER.list_configs( - env, - output, - config_name=config_name, - config_key=config_key, - config_type=config_type, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - - -@app.command('get', help='查看单个参数配置详情') -def get_command( - config_key: Annotated[ - str, - typer.Argument( - help='需要查询的参数键名', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys, - ), - ], - env: EnvOption = 'dev', - output: OutputOption = 'text', - source: Annotated[Literal['db', 'cache', 'both'], typer.Option('--source', help='读取来源')] = 'both', -) -> None: - """ - 查看单个参数配置详情。 - - :param config_key: 需要查询的参数键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param source: 配置读取来源 - :return: None - """ - _CONFIG_COMMAND_CONTROLLER.get_config(config_key, env, output, source=source) - - -@app.command('set', help='新增或更新单个参数配置') -def set_command( - config_key: Annotated[ - str, - typer.Argument( - help='需要写入的参数键名', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys, - ), - ], - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - value: Annotated[str, typer.Option('--value', help='参数键值')] = ..., - name: Annotated[str | None, typer.Option('--name', help='参数名称,新增时必填')] = None, - config_type: Annotated[Literal['Y', 'N'] | None, typer.Option('--config-type', help='系统内置标记')] = None, - remark: Annotated[str | None, typer.Option('--remark', help='参数备注')] = None, -) -> None: - """ - 新增或更新单个参数配置。 - - :param config_key: 需要写入的参数键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param value: 参数键值 - :param name: 参数名称 - :param config_type: 参数类型 - :param remark: 参数备注 - :return: None - """ - _CONFIG_COMMAND_CONTROLLER.set_config( - config_key, - env, - output, - allow_prod, - yes, - dry_run, - value=value, - name=name, - config_type=config_type, - remark=remark, - ) - - -@app.command('sync-cache', help='刷新参数配置缓存') -def sync_cache( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 刷新参数配置缓存。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _CONFIG_COMMAND_CONTROLLER.sync_cache(env, output, allow_prod, yes) - - -@app.command('doctor', help='诊断参数配置数据库与缓存是否一致') -def doctor( - env: EnvOption = 'dev', - output: OutputOption = 'text', - sample_limit: Annotated[int, typer.Option('--sample-limit', min=1, help='问题示例键名输出上限')] = 10, -) -> None: - """ - 诊断参数配置数据库与缓存是否一致。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param sample_limit: 问题示例键名输出上限 - :return: None - """ - _CONFIG_COMMAND_CONTROLLER.doctor(env, output, sample_limit=sample_limit) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/config/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/config/controller.py deleted file mode 100644 index 1404898..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/config/controller.py +++ /dev/null @@ -1,225 +0,0 @@ -from typing import Literal - -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.config import CONFIG_RUNTIME, ConfigRuntimeService - -from .presenter import ConfigCommandPresenter - - -class ConfigCommandController: - """ - 参数配置命令控制器。 - - 该控制器负责组织 `config` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 参数配置命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: ConfigCommandPresenter | None = None, - runtime_service: ConfigRuntimeService | None = None, - ) -> None: - """ - 初始化参数配置命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 参数配置命令文本渲染器 - :param runtime_service: 参数配置运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or ConfigCommandPresenter() - self.runtime_service = runtime_service or CONFIG_RUNTIME - - def list_configs( - self, - env: str, - output: str, - *, - config_name: str, - config_key: str, - config_type: Literal['Y', 'N'] | None, - begin_date: str, - end_date: str, - paged: bool, - page_num: int, - page_size: int, - ) -> None: - """ - 查看参数配置列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param config_name: 参数名称过滤条件 - :param config_key: 参数键名过滤条件 - :param config_type: 参数类型过滤条件 - :param begin_date: 查询开始日期 - :param end_date: 查询结束日期 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async( - self.runtime_service.list_configs( - config_name=config_name, - config_key=config_key, - config_type=config_type, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - ) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_config_list_text, - text_condition=lambda data: data.get('ok', False), - ) - - def get_config( - self, - config_key: str, - env: str, - output: str, - *, - source: Literal['db', 'cache', 'both'], - ) -> None: - """ - 查看单个参数配置详情。 - - :param config_key: 需要查询的参数键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param source: 配置读取来源 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.get_config(config_key, source=source)) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_config_get_text, - text_condition=lambda data: data.get('ok', False), - ) - - def set_config( - self, - config_key: str, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - value: str, - name: str | None, - config_type: Literal['Y', 'N'] | None, - remark: str | None, - ) -> None: - """ - 新增或更新单个参数配置。 - - :param config_key: 需要写入的参数键名 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param value: 参数键值 - :param name: 参数名称 - :param config_type: 参数类型 - :param remark: 参数备注 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='config set', - ) - payload = self.execution_service.run_async( - self.runtime_service.set_config( - config_key, - value, - config_name=name, - config_type=config_type, - remark=remark, - dry_run=dry_run, - ) - ) - payload['env'] = ctx.env - self.execution_service.complete_payload(ctx, payload) - - def sync_cache( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - ) -> None: - """ - 刷新参数配置缓存。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - False, - command_name='config sync-cache', - ) - payload = self.execution_service.run_async(self.runtime_service.sync_config_cache()) - payload['env'] = ctx.env - self.execution_service.complete_payload(ctx, payload) - - def doctor( - self, - env: str, - output: str, - *, - sample_limit: int, - ) -> None: - """ - 诊断参数配置数据库与缓存是否一致。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param sample_limit: 问题示例键名输出上限 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.diagnose_config(sample_limit=sample_limit)) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_config_doctor_text, - text_condition=lambda data: 'error' not in data, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/config/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/config/presenter.py deleted file mode 100644 index cde9693..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/config/presenter.py +++ /dev/null @@ -1,180 +0,0 @@ -from cli.utils import SHELL_TEXT_FORMATTER - - -class ConfigCommandPresenter: - """ - 参数配置命令文本渲染器。 - - 该渲染器负责将 `config` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_config_list_text(self, payload: dict[str, object]) -> str: - """ - 将参数配置列表结果渲染为文本摘要。 - - :param payload: 参数配置列表结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - ] - - filters = payload.get('filters') - if isinstance(filters, dict): - lines.extend(self._build_config_filter_lines(filters)) - - page_payload = payload.get('page') - if isinstance(page_payload, dict): - rows = page_payload.get('rows') - lines.append( - 'page: ' - f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} ' - f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})' - ) - lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}') - if not isinstance(rows, list) or not rows: - lines.append('configs: none') - return '\n'.join(lines) - lines.append('configs:') - for row in rows: - if isinstance(row, dict): - lines.extend([f' {item}' for item in self._build_config_item_lines(row)]) - return '\n'.join(lines) - - items = payload.get('items') - lines.append(f'count: {payload.get("count", 0)}') - if not isinstance(items, list) or not items: - lines.append('configs: none') - return '\n'.join(lines) - - lines.append('configs:') - for item in items: - if isinstance(item, dict): - lines.extend([f' {line}' for line in self._build_config_item_lines(item)]) - return '\n'.join(lines) - - def build_config_get_text(self, payload: dict[str, object]) -> str: - """ - 将单个参数配置详情结果渲染为文本摘要。 - - :param payload: 参数配置详情结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'key: {payload.get("key", "")}', - f'source: {payload.get("source", "")}', - ] - - if payload.get('source') == 'both': - lines.append(f'in_sync: {str(payload.get("inSync", False)).lower()}') - - if payload.get('source') in {'db', 'both'}: - lines.extend(self._build_config_detail_section('database', payload.get('database'))) - if payload.get('source') in {'cache', 'both'}: - lines.extend(self._build_config_detail_section('cache', payload.get('cache'))) - return '\n'.join(lines) - - def build_config_doctor_text(self, payload: dict[str, object]) -> str: - """ - 将参数配置诊断结果渲染为文本摘要。 - - :param payload: 参数配置诊断结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'message: {payload.get("message", "-")}', - f'database_count: {payload.get("databaseCount", 0)}', - f'cache_count: {payload.get("cacheCount", 0)}', - f'missing_in_cache_count: {payload.get("missingInCacheCount", 0)}', - f'orphan_in_cache_count: {payload.get("orphanInCacheCount", 0)}', - f'mismatch_count: {payload.get("mismatchCount", 0)}', - f'sample_limit: {payload.get("sampleLimit", 0)}', - ] - lines.extend(self._build_config_doctor_items('missing_in_cache', payload.get('missingInCache'))) - lines.extend(self._build_config_doctor_items('orphan_in_cache', payload.get('orphanInCache'))) - lines.extend(self._build_config_doctor_items('mismatch_keys', payload.get('mismatchKeys'))) - return '\n'.join(lines) - - @staticmethod - def _build_config_filter_lines(filters: dict[str, object]) -> list[str]: - """ - 构建参数配置过滤条件文本行。 - - :param filters: 过滤条件字典 - :return: 过滤条件文本行列表 - """ - active_filters = [] - for key, value in filters.items(): - if value in (None, '', False): - continue - active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}') - if not active_filters: - return ['filters: none'] - return ['filters:', *[f' - {item}' for item in active_filters]] - - @staticmethod - def _build_config_item_lines(config_item: dict[str, object]) -> list[str]: - """ - 构建单条参数配置记录的文本行。 - - :param config_item: 单条参数配置记录 - :return: 文本行列表 - """ - config_id = config_item.get('configId', '-') - config_key = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configKey', ''), 50) - config_name = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configName', ''), 24) - config_type = config_item.get('configType', '-') - config_value = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configValue', ''), 80) - remark = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('remark', ''), 60) - return [ - f'- [{config_id}] {config_key} | 名称: {config_name} | 内置: {config_type}', - f' value: {config_value or "-"}', - f' remark: {remark or "-"}', - ] - - @staticmethod - def _build_config_detail_section(title: str, config_item: dict[str, object] | None) -> list[str]: - """ - 构建单个配置来源的详情文本段落。 - - :param title: 段落标题 - :param config_item: 配置详情字典 - :return: 文本行列表 - """ - if not isinstance(config_item, dict): - return [f'{title}: none'] - - config_key = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configKey', ''), 60) - config_name = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configName', ''), 30) - config_value = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configValue', ''), 120) - config_type = config_item.get('configType', '-') - remark = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('remark', ''), 80) - config_id = config_item.get('configId', '-') - return [ - f'{title}:', - f' id: {config_id}', - f' key: {config_key or "-"}', - f' name: {config_name or "-"}', - f' value: {config_value or "-"}', - f' type: {config_type}', - f' remark: {remark or "-"}', - ] - - @staticmethod - def _build_config_doctor_items(title: str, items: list[object] | object) -> list[str]: - """ - 构建配置诊断问题示例段落。 - - :param title: 段落标题 - :param items: 问题键名列表 - :return: 文本行列表 - """ - if not isinstance(items, list) or not items: - return [f'{title}: none'] - return [f'{title}:', *[f' - {item}' for item in items]] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/command.py deleted file mode 100644 index 391199b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/command.py +++ /dev/null @@ -1,94 +0,0 @@ -import typer - -from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption - -from .controller import CryptoCommandController - -app = typer.Typer( - help='传输加密相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_CRYPTO_COMMAND_CONTROLLER = CryptoCommandController() - - -@app.command('validate', help='校验传输加密配置') -def validate( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 校验传输加密配置。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CRYPTO_COMMAND_CONTROLLER.validate(env, output) - - -@app.command('keygen', help='生成新的传输加密密钥对') -def keygen( - env: EnvOption = 'dev', - output: OutputOption = 'text', - kid: str = typer.Option('default', '--kid', help='生成的密钥版本标识'), - key_size: int = typer.Option(2048, '--key-size', help='RSA 密钥长度'), -) -> None: - """ - 生成新的传输加密密钥对。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param kid: 生成的密钥版本标识 - :param key_size: RSA 密钥长度 - :return: None - """ - _CRYPTO_COMMAND_CONTROLLER.keygen(env, output, kid=kid, key_size=key_size) - - -@app.command('rotate', help='生成传输加密密钥轮换辅助结果') -def rotate( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - next_kid: str = typer.Option('rotated', '--next-kid', help='轮换后的新密钥版本标识'), - key_size: int = typer.Option(2048, '--key-size', help='新密钥的 RSA 长度'), -) -> None: - """ - 生成传输加密密钥轮换辅助结果。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param next_kid: 轮换后的新密钥版本标识 - :param key_size: 新密钥的 RSA 长度 - :return: None - """ - _CRYPTO_COMMAND_CONTROLLER.rotate( - env, - output, - allow_prod, - yes, - dry_run, - next_kid=next_kid, - key_size=key_size, - ) - - -@app.command('export-public', help='查看当前运行环境的公钥信息') -def export_public( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 导出当前运行环境的公钥信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _CRYPTO_COMMAND_CONTROLLER.export_public(env, output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/controller.py deleted file mode 100644 index 919e4f4..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/controller.py +++ /dev/null @@ -1,125 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService - -from .presenter import CryptoCommandPresenter - - -class CryptoCommandController: - """ - 传输加密命令控制器。 - - 该控制器负责组织 `crypto` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 传输加密命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: CryptoCommandPresenter | None = None, - runtime_service: CryptoRuntimeService | None = None, - ) -> None: - """ - 初始化传输加密命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 传输加密命令文本渲染器 - :param runtime_service: 传输加密运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or CryptoCommandPresenter() - self.runtime_service = runtime_service or CRYPTO_RUNTIME - - def validate(self, env: str, output: str) -> None: - """ - 校验传输加密配置。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload(ctx, self.runtime_service.validate_crypto_config()) - - def keygen(self, env: str, output: str, *, kid: str, key_size: int) -> None: - """ - 生成新的传输加密密钥对。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param kid: 生成的密钥版本标识 - :param key_size: RSA 密钥长度 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.generate_crypto_key_pair(kid, key_size) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_crypto_keygen_text, - text_condition=lambda data: data.get('ok', False), - ) - - def rotate( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - next_kid: str, - key_size: int, - ) -> None: - """ - 生成传输加密密钥轮换辅助结果。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param next_kid: 轮换后的新密钥版本标识 - :param key_size: 新密钥的 RSA 长度 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='crypto rotate', - ) - self.execution_service.complete_payload(ctx, self.runtime_service.build_rotation_payload(next_kid, key_size)) - - def export_public(self, env: str, output: str) -> None: - """ - 导出当前运行环境的公钥信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.export_public_key() - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_export_public_text, - text_condition=lambda data: data.get('ok', False), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/presenter.py deleted file mode 100644 index cf5f542..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/crypto/presenter.py +++ /dev/null @@ -1,79 +0,0 @@ -class CryptoCommandPresenter: - """ - 传输加密命令文本渲染器。 - - 该渲染器负责将 `crypto` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_crypto_keygen_text(self, payload: dict[str, object]) -> str: - """ - 将密钥生成结果渲染为文本摘要。 - - :param payload: 密钥生成结果字典 - :return: 文本摘要 - """ - env_patch = payload.get('envPatch') - env_patch_keys = sorted(env_patch.keys()) if isinstance(env_patch, dict) else [] - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'kid: {payload.get("kid", "-")}', - f'key_size: {payload.get("keySize", "-")}', - ] - lines.extend(self._build_multiline_block('public_key', payload.get('publicKey'))) - lines.extend(self._build_multiline_block('private_key', payload.get('privateKey'))) - if env_patch_keys: - lines.append('env_patch_keys:') - lines.extend(f' - {key}' for key in env_patch_keys) - else: - lines.append('env_patch_keys: none') - return '\n'.join(lines) - - def build_export_public_text(self, payload: dict[str, object]) -> str: - """ - 将公钥导出结果渲染为文本摘要。 - - :param payload: 公钥导出结果字典 - :return: 文本摘要 - """ - public_key_payload = payload.get('publicKey') - if not isinstance(public_key_payload, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'public_key: none', - ] - ) - - supported_kids = public_key_payload.get('supportedKids') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'kid: {public_key_payload.get("kid", "-")}', - f'alg: {public_key_payload.get("alg", "-")}', - f'envelope_version: {public_key_payload.get("envelopeVersion", "-")}', - f'expire_at: {public_key_payload.get("expireAt", "-")}', - ] - if isinstance(supported_kids, list) and supported_kids: - lines.append('supported_kids:') - lines.extend(f' - {kid}' for kid in supported_kids) - else: - lines.append('supported_kids: none') - lines.extend(self._build_multiline_block('public_key_pem', public_key_payload.get('publicKey'))) - return '\n'.join(lines) - - @staticmethod - def _build_multiline_block(title: str, value: object) -> list[str]: - """ - 将多行文本构建为带标题的块级文本。 - - :param title: 块标题 - :param value: 原始文本值 - :return: 文本行列表 - """ - text = '' if value is None else str(value).strip() - if not text: - return [f'{title}: -'] - return [f'{title}:', ' |', *[f' {line}' for line in text.splitlines()]] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/db/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/db/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/db/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/db/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/db/command.py deleted file mode 100644 index 80063de..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/db/command.py +++ /dev/null @@ -1,191 +0,0 @@ -from typing import Annotated - -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY -from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption - -from .controller import DbCommandController - -app = typer.Typer( - help='数据库相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_DB_COMMAND_CONTROLLER = DbCommandController() - - -@app.command('check', help='检查数据库连接状态') -def check( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 检查数据库连接状态。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _DB_COMMAND_CONTROLLER.check(env, output) - - -@app.command('current', help='查看当前数据库迁移版本') -def current( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看数据库当前迁移版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _DB_COMMAND_CONTROLLER.current(env, output) - - -@app.command('upgrade', help='执行数据库升级') -def upgrade( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - revision: Annotated[ - str, - typer.Option( - '--revision', - help='目标迁移版本', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_alembic_revisions, - ), - ] = 'head', -) -> None: - """ - 执行数据库升级。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param revision: 目标迁移版本 - :return: None - """ - _DB_COMMAND_CONTROLLER.upgrade(env, output, allow_prod, yes, dry_run, revision=revision) - - -@app.command('init', help='初始化数据库到最新迁移版本') -def init( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, -) -> None: - """ - 初始化数据库到最新迁移版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :return: None - """ - _DB_COMMAND_CONTROLLER.init(env, output, allow_prod, yes, dry_run) - - -@app.command('downgrade', help='执行数据库回退') -def downgrade( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - revision: Annotated[ - str, - typer.Option( - '--revision', - help='目标回退版本', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_alembic_revisions, - ), - ] = '-1', -) -> None: - """ - 执行数据库回退。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param revision: 目标回退版本 - :return: None - """ - _DB_COMMAND_CONTROLLER.downgrade(env, output, allow_prod, yes, dry_run, revision=revision) - - -@app.command('revision', help='创建新的数据库迁移版本文件') -def revision( - message: Annotated[str, typer.Option('--message', '-m', help='迁移说明')], - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - autogenerate: bool = typer.Option(False, '--autogenerate/--no-autogenerate', help='是否自动生成迁移内容'), -) -> None: - """ - 创建新的数据库迁移版本文件。 - - :param message: 迁移说明 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param autogenerate: 是否自动生成迁移内容 - :return: None - """ - _DB_COMMAND_CONTROLLER.revision( - message, - env, - output, - allow_prod, - yes, - dry_run, - autogenerate=autogenerate, - ) - - -@app.command('heads', help='查看当前代码仓库中的 Alembic heads') -def heads( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看当前代码仓库中的 Alembic heads。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _DB_COMMAND_CONTROLLER.heads(env, output) - - -@app.command('history', help='查看当前代码仓库中的 Alembic 历史版本') -def history( - env: EnvOption = 'dev', - output: OutputOption = 'text', - limit: Annotated[int, typer.Option('--limit', min=1, help='输出的最大历史记录数量')] = 20, -) -> None: - """ - 查看当前代码仓库中的 Alembic 历史版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param limit: 输出的最大历史记录数量 - :return: None - """ - _DB_COMMAND_CONTROLLER.history(env, output, limit=limit) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/db/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/db/controller.py deleted file mode 100644 index 7e4bb3c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/db/controller.py +++ /dev/null @@ -1,237 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService - -from .presenter import DbCommandPresenter - - -class DbCommandController: - """ - 数据库命令控制器。 - - 该控制器负责组织 `db` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 数据库命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: DbCommandPresenter | None = None, - runtime_service: DatabaseRuntimeService | None = None, - ) -> None: - """ - 初始化数据库命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 数据库命令文本渲染器 - :param runtime_service: 数据库运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or DbCommandPresenter() - self.runtime_service = runtime_service or DATABASE_RUNTIME - - def check(self, env: str, output: str) -> None: - """ - 检查数据库连接状态。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.runtime_service.ping_database()) - ) - - def current(self, env: str, output: str) -> None: - """ - 查看数据库当前迁移版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.get_current_revision() - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_current_revision_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def upgrade( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - revision: str, - ) -> None: - """ - 执行数据库升级。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param revision: 目标迁移版本 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='db upgrade', - ) - self.execution_service.complete_payload(ctx, self.runtime_service.upgrade_database(revision, dry_run=dry_run)) - - def init( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> None: - """ - 初始化数据库到最新迁移版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='db init', - ) - self.execution_service.complete_payload(ctx, self.runtime_service.init_database(dry_run=dry_run)) - - def downgrade( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - revision: str, - ) -> None: - """ - 执行数据库回退。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param revision: 目标回退版本 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='db downgrade', - ) - self.execution_service.complete_payload(ctx, self.runtime_service.downgrade_database(revision, dry_run=dry_run)) - - def revision( - self, - message: str, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - autogenerate: bool, - ) -> None: - """ - 创建新的数据库迁移版本文件。 - - :param message: 迁移说明 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param autogenerate: 是否自动生成迁移内容 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='db revision', - ) - self.execution_service.complete_payload( - ctx, - self.runtime_service.create_revision(message, autogenerate=autogenerate, dry_run=dry_run), - ) - - def heads(self, env: str, output: str) -> None: - """ - 查看当前代码仓库中的 Alembic heads。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.get_alembic_heads() - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_alembic_revisions_text, - text_condition=lambda result_data: 'error' not in result_data, - ) - - def history(self, env: str, output: str, *, limit: int) -> None: - """ - 查看当前代码仓库中的 Alembic 历史版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param limit: 输出的最大历史记录数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.get_alembic_history(limit=limit) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_alembic_revisions_text, - text_condition=lambda result_data: 'error' not in result_data, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/db/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/db/presenter.py deleted file mode 100644 index 72d04b1..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/db/presenter.py +++ /dev/null @@ -1,71 +0,0 @@ -class DbCommandPresenter: - """ - 数据库命令文本渲染器。 - - 该渲染器负责将 `db` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - @staticmethod - def build_current_revision_text(payload: dict[str, object]) -> str: - """ - 将数据库当前迁移版本结果渲染为文本摘要。 - - :param payload: 当前迁移版本结果字典 - :return: 文本摘要 - """ - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'current_revision: {payload.get("currentRevision", "-")}', - ] - ) - - def build_alembic_revisions_text(self, payload: dict[str, object]) -> str: - """ - 将 Alembic 修订版本结果渲染为文本摘要。 - - :param payload: 修订版本结果字典 - :return: 文本摘要 - """ - items = payload.get('items') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'message: {payload.get("message", "-")}', - f'count: {payload.get("count", 0)}', - ] - if 'totalCount' in payload: - lines.append(f'total_count: {payload.get("totalCount", 0)}') - if 'limit' in payload: - lines.append(f'limit: {payload.get("limit", 0)}') - if not isinstance(items, list) or not items: - lines.append('items: none') - return '\n'.join(lines) - - lines.append('items:') - for item in items: - if isinstance(item, dict): - lines.extend([f' {line}' for line in self._build_alembic_revision_item_lines(item)]) - return '\n'.join(lines) - - @staticmethod - def _build_alembic_revision_item_lines(revision_item: dict[str, object]) -> list[str]: - """ - 构建单个 Alembic 修订版本的文本行。 - - :param revision_item: 修订版本结果字典 - :return: 文本行列表 - """ - down_revisions = revision_item.get('downRevisions') - branch_labels = revision_item.get('branchLabels') - depends_on = revision_item.get('dependsOn') - return [ - f'- revision: {revision_item.get("revision", "-")}', - f' down_revisions: {",".join(down_revisions) if isinstance(down_revisions, list) and down_revisions else "-"}', - f' branch_labels: {",".join(branch_labels) if isinstance(branch_labels, list) and branch_labels else "-"}', - f' depends_on: {",".join(depends_on) if isinstance(depends_on, list) and depends_on else "-"}', - f' doc: {revision_item.get("doc", "") or "-"}', - f' path: {revision_item.get("path", "-")}', - ] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/dev/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/dev/command.py deleted file mode 100644 index 0a0f905..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/command.py +++ /dev/null @@ -1,74 +0,0 @@ -from typing import Annotated - -import typer - -from cli.context import EnvOption, OutputOption - -from .controller import DevCommandController - -app = typer.Typer( - help='开发相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_DEV_COMMAND_CONTROLLER = DevCommandController() - - -@app.command('lint', help='执行 Ruff 格式化与静态检查') -def lint( - targets: Annotated[list[str] | None, typer.Argument(help='待检查路径,可传多个')] = None, - env: EnvOption = 'dev', - output: OutputOption = 'text', - check_only: bool = typer.Option(False, '--check-only', help='仅检查,不写回格式化结果'), - fix: bool = typer.Option(False, '--fix', help='执行 ruff check --fix'), - unsafe_fixes: bool = typer.Option(False, '--unsafe-fixes', help='允许 Ruff 不安全修复'), -) -> None: - """ - 执行 Ruff 格式化与静态检查。 - - :param targets: 待检查路径列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param check_only: 是否仅检查不写回 - :param fix: 是否自动修复可修复问题 - :param unsafe_fixes: 是否允许不安全修复 - :return: None - """ - _DEV_COMMAND_CONTROLLER.lint( - targets, - env, - output, - check_only=check_only, - fix=fix, - unsafe_fixes=unsafe_fixes, - ) - - -@app.command('test', help='执行项目测试') -def test( - targets: Annotated[list[str] | None, typer.Argument(help='待执行的测试路径,可传多个')] = None, - env: EnvOption = 'dev', - output: OutputOption = 'text', - keyword: str = typer.Option('', '--keyword', '-k', help='pytest -k 过滤表达式'), - maxfail: int = typer.Option(0, '--maxfail', min=0, help='最大失败数,0 表示不限制'), - quiet: bool = typer.Option(False, '--quiet', '-q', help='启用简洁输出'), -) -> None: - """ - 执行项目测试。 - - :param targets: 测试目标路径列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param keyword: pytest 关键字过滤表达式 - :param maxfail: 最大失败数 - :param quiet: 是否启用简洁输出 - :return: None - """ - _DEV_COMMAND_CONTROLLER.test( - targets, - env, - output, - keyword=keyword, - maxfail=maxfail, - quiet=quiet, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/dev/controller.py deleted file mode 100644 index ccd8682..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/controller.py +++ /dev/null @@ -1,103 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.dev import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService - -from .presenter import DevCommandPresenter - - -class DevCommandController: - """ - 开发命令控制器。 - - 该控制器负责组织 `dev` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 开发命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: DevCommandPresenter | None = None, - runtime_service: DevelopmentRuntimeService | None = None, - ) -> None: - """ - 初始化开发命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 开发命令文本渲染器 - :param runtime_service: 开发运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or DevCommandPresenter() - self.runtime_service = runtime_service or DEVELOPMENT_RUNTIME - - def lint( - self, - targets: list[str] | None, - env: str, - output: str, - *, - check_only: bool, - fix: bool, - unsafe_fixes: bool, - ) -> None: - """ - 执行 Ruff 格式化与静态检查。 - - :param targets: 待检查路径列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param check_only: 是否仅检查不写回 - :param fix: 是否自动修复可修复问题 - :param unsafe_fixes: 是否允许不安全修复 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.run_lint(targets, check_only=check_only, fix=fix, unsafe_fixes=unsafe_fixes) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_dev_lint_text, - ) - - def test( - self, - targets: list[str] | None, - env: str, - output: str, - *, - keyword: str, - maxfail: int, - quiet: bool, - ) -> None: - """ - 执行项目测试。 - - :param targets: 测试目标路径列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param keyword: pytest 关键字过滤表达式 - :param maxfail: 最大失败数 - :param quiet: 是否启用简洁输出 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.runtime_service.run_tests(targets, keyword=keyword, maxfail=maxfail, quiet=quiet) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_dev_test_text, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/dev/presenter.py deleted file mode 100644 index b5fc144..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/dev/presenter.py +++ /dev/null @@ -1,96 +0,0 @@ -class DevCommandPresenter: - """ - 开发命令文本渲染器。 - - 该渲染器负责将 `dev` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_dev_lint_text(self, payload: dict[str, object]) -> str: - """ - 将 lint 执行结果渲染为文本摘要。 - - :param payload: lint 执行结果字典 - :return: 文本摘要 - """ - targets = payload.get('targets') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'check_only: {str(payload.get("checkOnly", False)).lower()}', - f'fix: {str(payload.get("fix", False)).lower()}', - f'unsafe_fixes: {str(payload.get("unsafeFixes", False)).lower()}', - ] - if isinstance(targets, list) and targets: - lines.append('targets:') - lines.extend(f' - {target}' for target in targets) - else: - lines.append('targets: none') - lines.extend(self._build_command_result_section('format', payload.get('format'))) - lines.extend(self._build_command_result_section('check', payload.get('check'))) - return '\n'.join(lines) - - def build_dev_test_text(self, payload: dict[str, object]) -> str: - """ - 将测试执行结果渲染为文本摘要。 - - :param payload: 测试执行结果字典 - :return: 文本摘要 - """ - targets = payload.get('targets') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'keyword: {payload.get("keyword", "") or "-"}', - f'maxfail: {payload.get("maxfail", 0)}', - f'quiet: {str(payload.get("quiet", False)).lower()}', - ] - if isinstance(targets, list) and targets: - lines.append('targets:') - lines.extend(f' - {target}' for target in targets) - else: - lines.append('targets: none') - lines.extend(self._build_command_result_section('test', payload.get('test'))) - return '\n'.join(lines) - - @staticmethod - def _build_command_result_section(title: str, payload: dict[str, object] | None) -> list[str]: - """ - 构建单个子命令执行结果段落。 - - :param title: 段落标题 - :param payload: 子命令执行结果 - :return: 文本行列表 - """ - if not isinstance(payload, dict): - return [f'{title}: none'] - - lines = [ - f'{title}:', - f' ok: {str(payload.get("ok", False)).lower()}', - f' return_code: {payload.get("returnCode", "-")}', - f' command: {DevCommandPresenter._format_command_text(payload.get("command"))}', - ] - stdout = payload.get('stdout') - stderr = payload.get('stderr') - if stdout: - lines.append(' stdout:') - lines.append(' |') - lines.extend(f' {line}' for line in str(stdout).splitlines()) - if stderr: - lines.append(' stderr:') - lines.append(' |') - lines.extend(f' {line}' for line in str(stderr).splitlines()) - return lines - - @staticmethod - def _format_command_text(command: object) -> str: - """ - 将命令参数列表格式化为可读文本。 - - :param command: 命令参数列表 - :return: 格式化后的命令文本 - """ - if not isinstance(command, list): - return '-' - return ' '.join(str(item) for item in command) or '-' diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/gen/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/gen/command.py deleted file mode 100644 index e4aac33..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/command.py +++ /dev/null @@ -1,273 +0,0 @@ -from typing import Annotated, Literal - -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY -from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption - -from .controller import GenCommandController - -app = typer.Typer( - help='代码生成相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_GEN_COMMAND_CONTROLLER = GenCommandController() - - -@app.command('import-table', help='导入数据库表到代码生成业务表') -def import_table( - table_names: Annotated[ - list[str], - typer.Argument( - help='待导入的数据库表名,可传多个', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_db_table_names, - ), - ], - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, -) -> None: - """ - 导入数据库表到代码生成业务表。 - - :param table_names: 待导入表名列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :return: None - """ - _GEN_COMMAND_CONTROLLER.import_table( - table_names, - env, - output, - allow_prod, - yes, - dry_run, - ) - - -@app.command('list', help='查看代码生成业务表列表') -def list_command( - env: EnvOption = 'dev', - output: OutputOption = 'text', - table_name: str = typer.Option( - '', - '--table-name', - help='按表名称过滤', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names, - ), - table_comment: str = typer.Option('', '--table-comment', help='按表描述过滤'), - paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'), - page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'), - page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'), -) -> None: - """ - 查看代码生成业务表列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - _GEN_COMMAND_CONTROLLER.list_tables( - env, - output, - table_name=table_name, - table_comment=table_comment, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - - -@app.command('db-list', help='查看数据库中可导入的物理表列表') -def db_list( - env: EnvOption = 'dev', - output: OutputOption = 'text', - table_name: str = typer.Option( - '', - '--table-name', - help='按表名称过滤', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_db_table_names, - ), - table_comment: str = typer.Option('', '--table-comment', help='按表描述过滤'), - paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'), - page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'), - page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'), -) -> None: - """ - 查看数据库中可导入的物理表列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - _GEN_COMMAND_CONTROLLER.list_db_tables( - env, - output, - table_name=table_name, - table_comment=table_comment, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - - -@app.command('detail', help='查看单个代码生成业务表详情') -def detail( - table_id: Annotated[int, typer.Argument(help='业务表 ID')], - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看单个代码生成业务表详情。 - - :param table_id: 业务表 ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _GEN_COMMAND_CONTROLLER.show_detail(table_id, env, output) - - -@app.command('create-table', help='根据建表 SQL 创建表结构并导入代码生成业务表') -def create_table( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - sql: str = typer.Option('', '--sql', help='直接传入建表 SQL 文本'), - sql_file: str = typer.Option( - '', - '--sql-file', - help='从文件读取建表 SQL', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_sql_files, - ), -) -> None: - """ - 根据建表 SQL 创建表结构并导入代码生成业务表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param sql: 直接传入的 SQL 文本 - :param sql_file: SQL 文件路径 - :return: None - """ - _GEN_COMMAND_CONTROLLER.create_table( - env, - output, - allow_prod, - yes, - dry_run, - sql=sql, - sql_file=sql_file, - ) - - -@app.command('preview', help='查看指定业务表的代码生成结果预览') -def preview( - table_id: Annotated[int, typer.Argument(help='业务表 ID')], - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 预览指定业务表的代码生成结果。 - - :param table_id: 业务表 ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _GEN_COMMAND_CONTROLLER.preview(table_id, env, output) - - -@app.command('export', help='导出代码生成结果') -def export( - table_names: Annotated[ - list[str], - typer.Argument( - help='待导出的业务表名,可传多个', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names, - ), - ], - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, - dry_run: DryRunOption = False, - mode: Annotated[Literal['zip', 'local'], typer.Option('--mode', help='导出模式')] = 'zip', - output_file: str = typer.Option( - '', - '--output-file', - help='zip 导出目标文件路径', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_output_paths, - ), -) -> None: - """ - 导出代码生成结果。 - - :param table_names: 业务表名称列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param mode: 导出模式 - :param output_file: zip 导出目标文件路径 - :return: None - """ - _GEN_COMMAND_CONTROLLER.export( - table_names, - env, - output, - allow_prod, - yes, - dry_run, - mode=mode, - output_file=output_file, - ) - - -@app.command('sync-db', help='同步指定业务表的数据库表结构') -def sync_db( - table_name: Annotated[ - str, - typer.Argument( - help='业务表名称', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names, - ), - ], - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 同步指定业务表的数据库表结构。 - - :param table_name: 业务表名称 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _GEN_COMMAND_CONTROLLER.sync_db(table_name, env, output, allow_prod, yes) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/gen/controller.py deleted file mode 100644 index 84fb446..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/controller.py +++ /dev/null @@ -1,304 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.gen import GEN_RUNTIME, GenRuntimeService - -from .presenter import GenCommandPresenter - - -class GenCommandController: - """ - 代码生成命令控制器。 - - 该控制器负责组织 `gen` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 代码生成命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: GenCommandPresenter | None = None, - runtime_service: GenRuntimeService | None = None, - ) -> None: - """ - 初始化代码生成命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 代码生成命令文本渲染器 - :param runtime_service: 代码生成运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or GenCommandPresenter() - self.runtime_service = runtime_service or GEN_RUNTIME - - def import_table( - self, - table_names: list[str], - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> None: - """ - 导入数据库表到代码生成业务表。 - - :param table_names: 待导入表名列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='gen import-table', - ) - payload = self.execution_service.run_async(self.runtime_service.import_tables(table_names, dry_run=dry_run)) - payload['env'] = ctx.env - self.execution_service.complete_payload(ctx, payload) - - def list_tables( - self, - env: str, - output: str, - *, - table_name: str, - table_comment: str, - paged: bool, - page_num: int, - page_size: int, - ) -> None: - """ - 查看代码生成业务表列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async( - self.runtime_service.list_gen_tables( - table_name=table_name, - table_comment=table_comment, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - ) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=lambda data: self.presenter.build_gen_table_list_text(data, db_mode=False), - text_condition=lambda data: data.get('ok', False), - ) - - def list_db_tables( - self, - env: str, - output: str, - *, - table_name: str, - table_comment: str, - paged: bool, - page_num: int, - page_size: int, - ) -> None: - """ - 查看数据库中可导入的物理表列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async( - self.runtime_service.list_gen_db_tables( - table_name=table_name, - table_comment=table_comment, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - ) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=lambda data: self.presenter.build_gen_table_list_text(data, db_mode=True), - text_condition=lambda data: data.get('ok', False), - ) - - def show_detail(self, table_id: int, env: str, output: str) -> None: - """ - 查看单个代码生成业务表详情。 - - :param table_id: 业务表 ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.get_gen_table_detail(table_id)) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_gen_detail_text, - text_condition=lambda data: 'error' not in data, - ) - - def create_table( - self, - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - sql: str, - sql_file: str, - ) -> None: - """ - 根据建表 SQL 创建表结构并导入代码生成业务表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param sql: 直接传入的 SQL 文本 - :param sql_file: SQL 文件路径 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='gen create-table', - ) - payload = self.execution_service.run_async(self.runtime_service.create_tables(sql, sql_file, dry_run=dry_run)) - payload['env'] = ctx.env - self.execution_service.complete_payload(ctx, payload) - - def preview(self, table_id: int, env: str, output: str) -> None: - """ - 预览指定业务表的代码生成结果。 - - :param table_id: 业务表 ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.runtime_service.preview_code(table_id)) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_gen_preview_text, - text_condition=lambda data: data.get('ok', False), - ) - - def export( - self, - table_names: list[str], - env: str, - output: str, - allow_prod: bool, - yes: bool, - dry_run: bool, - *, - mode: str, - output_file: str, - ) -> None: - """ - 导出代码生成结果。 - - :param table_names: 业务表名称列表 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :param dry_run: 是否演练执行 - :param mode: 导出模式 - :param output_file: zip 导出目标文件路径 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - dry_run, - command_name='gen export', - ) - payload = self.execution_service.run_async( - self.runtime_service.export_code(table_names, mode=mode, output_file=output_file, dry_run=dry_run) - ) - payload['env'] = ctx.env - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_gen_export_text, - text_condition=lambda data: data.get('ok', False), - ) - - def sync_db( - self, - table_name: str, - env: str, - output: str, - allow_prod: bool, - yes: bool, - ) -> None: - """ - 同步指定业务表的数据库表结构。 - - :param table_name: 业务表名称 - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous( - env, - output, - allow_prod, - yes, - False, - command_name='gen sync-db', - ) - payload = self.execution_service.run_async(self.runtime_service.sync_gen_table_from_db(table_name)) - payload['env'] = ctx.env - self.execution_service.complete_payload(ctx, payload) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/gen/presenter.py deleted file mode 100644 index 92844bf..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/gen/presenter.py +++ /dev/null @@ -1,231 +0,0 @@ -from cli.utils import SHELL_TEXT_FORMATTER - - -class GenCommandPresenter: - """ - 代码生成命令文本渲染器。 - - 该渲染器负责将 `gen` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_gen_preview_text(self, payload: dict[str, object]) -> str: - """ - 将代码预览结果渲染为文本摘要。 - - :param payload: 代码预览结果字典 - :return: 文本摘要 - """ - preview_payload = payload.get('preview') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'table_id: {payload.get("tableId", "-")}', - f'template_count: {payload.get("templateCount", 0)}', - ] - if not isinstance(preview_payload, dict) or not preview_payload: - lines.append('templates: none') - return '\n'.join(lines) - - lines.append('templates:') - for template_name, template_content in preview_payload.items(): - lines.extend([f' {line}' for line in self._build_text_block(str(template_name), template_content)]) - return '\n'.join(lines) - - def build_gen_export_text(self, payload: dict[str, object]) -> str: - """ - 将代码导出结果渲染为文本摘要。 - - :param payload: 代码导出结果字典 - :return: 文本摘要 - """ - table_names = payload.get('tableNames') - results = payload.get('results') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - f'mode: {payload.get("mode", "-")}', - f'dry_run: {str(payload.get("dryRun", False)).lower()}', - f'message: {payload.get("message", "-")}', - ] - if isinstance(table_names, list) and table_names: - lines.append('table_names:') - lines.extend(f' - {table_name}' for table_name in table_names) - else: - lines.append('table_names: none') - - if payload.get('outputFile'): - lines.append(f'output_file: {payload.get("outputFile")}') - if payload.get('genPath'): - lines.append(f'gen_path: {payload.get("genPath")}') - if payload.get('size') is not None: - lines.append(f'size: {payload.get("size")}') - - if isinstance(results, list) and results: - lines.append('results:') - lines.extend( - f' - {item.get("tableName", "-")}: {str(item.get("ok", False)).lower()} | {item.get("message", "-")}' - for item in results - if isinstance(item, dict) - ) - return '\n'.join(lines) - - def build_gen_table_list_text(self, payload: dict[str, object], *, db_mode: bool = False) -> str: - """ - 将代码生成表列表或数据库表列表渲染为文本摘要。 - - :param payload: 列表结果字典 - :param db_mode: 是否为数据库物理表列表模式 - :return: 文本摘要 - """ - lines = [f'ok: {str(payload.get("ok", False)).lower()}'] - filters = payload.get('filters') - if isinstance(filters, dict): - lines.extend(self._build_gen_filter_lines(filters)) - - page_payload = payload.get('page') - if isinstance(page_payload, dict): - rows = page_payload.get('rows', []) - lines.append( - 'page: ' - f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} ' - f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})' - ) - lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}') - if not isinstance(rows, list) or not rows: - lines.append('items: none') - return '\n'.join(lines) - lines.append('items:') - for row in rows: - if isinstance(row, dict): - item_lines = ( - self._build_gen_db_table_item_lines(row) if db_mode else self._build_gen_table_item_lines(row) - ) - lines.extend([f' {item}' for item in item_lines]) - return '\n'.join(lines) - - items = payload.get('items') - lines.append(f'count: {payload.get("count", 0)}') - if not isinstance(items, list) or not items: - lines.append('items: none') - return '\n'.join(lines) - - lines.append('items:') - for item in items: - if isinstance(item, dict): - item_lines = ( - self._build_gen_db_table_item_lines(item) if db_mode else self._build_gen_table_item_lines(item) - ) - lines.extend([f' {line}' for line in item_lines]) - return '\n'.join(lines) - - def build_gen_detail_text(self, payload: dict[str, object]) -> str: - """ - 将代码生成业务表详情渲染为文本摘要。 - - :param payload: 详情结果字典 - :return: 文本摘要 - """ - detail = payload.get('detail') - if not isinstance(detail, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'table_id: {payload.get("tableId", "-")}', - 'detail: none', - ] - ) - - info = detail.get('info') - if not isinstance(info, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'table_id: {payload.get("tableId", "-")}', - 'info: none', - ] - ) - - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'table_id: {payload.get("tableId", "-")}', - f'table_name: {payload.get("tableName", "-")}', - f'column_count: {payload.get("columnCount", 0)}', - f'table_count: {payload.get("tableCount", 0)}', - f'table_comment: {SHELL_TEXT_FORMATTER.truncate_text(info.get("tableComment", ""), 80) or "-"}', - f'class_name: {info.get("className", "-")}', - f'tpl_category: {info.get("tplCategory", "-")}', - f'tpl_web_type: {info.get("tplWebType", "-")}', - f'package_name: {SHELL_TEXT_FORMATTER.truncate_text(info.get("packageName", ""), 80) or "-"}', - f'module_name: {info.get("moduleName", "-")}', - f'business_name: {info.get("businessName", "-")}', - f'function_name: {info.get("functionName", "-")}', - f'function_author: {info.get("functionAuthor", "-")}', - f'gen_type: {info.get("genType", "-")}', - f'gen_path: {SHELL_TEXT_FORMATTER.truncate_text(info.get("genPath", ""), 120) or "-"}', - f'remark: {SHELL_TEXT_FORMATTER.truncate_text(info.get("remark", ""), 160) or "-"}', - ] - ) - - @staticmethod - def _build_text_block(title: str, value: object) -> list[str]: - """ - 将多行文本构建为带标题的块级文本。 - - :param title: 块标题 - :param value: 原始文本值 - :return: 文本行列表 - """ - text = '' if value is None else str(value) - if not text.strip(): - return [f'{title}: -'] - return [f'{title}:', ' |', *[f' {line}' for line in text.splitlines()]] - - @staticmethod - def _build_gen_filter_lines(filters: dict[str, object]) -> list[str]: - """ - 构建代码生成表过滤条件文本行。 - - :param filters: 过滤条件字典 - :return: 过滤条件文本行列表 - """ - active_filters = [] - for key, value in filters.items(): - if value in (None, '', False): - continue - active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}') - if not active_filters: - return ['filters: none'] - return ['filters:', *[f' - {item}' for item in active_filters]] - - @staticmethod - def _build_gen_table_item_lines(table_item: dict[str, object]) -> list[str]: - """ - 构建单条代码生成表记录的文本行。 - - :param table_item: 代码生成表记录 - :return: 文本行列表 - """ - return [ - f'- [{table_item.get("tableId", "-")}] {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableName", ""), 40) or "-"}', - f' comment: {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableComment", ""), 60) or "-"}', - f' class_name: {table_item.get("className", "-")}', - f' tpl_category: {table_item.get("tplCategory", "-")} | module_name: {table_item.get("moduleName", "-")}', - f' business_name: {table_item.get("businessName", "-")} | function_name: {table_item.get("functionName", "-")}', - ] - - @staticmethod - def _build_gen_db_table_item_lines(table_item: dict[str, object]) -> list[str]: - """ - 构建单条数据库物理表记录的文本行。 - - :param table_item: 数据库物理表记录 - :return: 文本行列表 - """ - return [ - f'- {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableName", ""), 48) or "-"}', - f' comment: {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableComment", ""), 80) or "-"}', - f' create_time: {table_item.get("createTime", "-")}', - f' update_time: {table_item.get("updateTime", "-")}', - ] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/job/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/job/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/job/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/job/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/job/command.py deleted file mode 100644 index af62ea7..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/job/command.py +++ /dev/null @@ -1,216 +0,0 @@ -import typer - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY -from cli.context import AllowProdOption, EnvOption, OutputOption, YesOption - -from .controller import JobCommandController - -app = typer.Typer( - help='定时任务相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_JOB_COMMAND_CONTROLLER = JobCommandController() - - -@app.command('list', help='查看定时任务列表') -def list_command( - env: EnvOption = 'dev', - output: OutputOption = 'text', - job_name: str = typer.Option( - '', - '--job-name', - help='按任务名称过滤', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_names, - ), - job_group: str = typer.Option('', '--job-group', help='按任务组过滤'), - status: str | None = typer.Option(None, '--status', help='按任务状态过滤,0正常 1暂停'), - paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'), - page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'), - page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'), -) -> None: - """ - 查看定时任务列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 状态过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - _JOB_COMMAND_CONTROLLER.list( - env, - output, - job_name=job_name, - job_group=job_group, - status=status, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - - -@app.command('run-once', help='执行一次定时任务') -def run_once( - job_id: int = typer.Argument( - ..., - help='任务ID', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 执行一次定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _JOB_COMMAND_CONTROLLER.run_once(job_id, env, output, allow_prod, yes) - - -@app.command('detail', help='查看单个定时任务详情') -def detail( - job_id: int = typer.Argument( - ..., - help='任务ID', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 查看单个定时任务详情。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _JOB_COMMAND_CONTROLLER.detail(job_id, env, output) - - -@app.command('logs', help='查看定时任务执行日志列表') -def logs( - env: EnvOption = 'dev', - output: OutputOption = 'text', - job_name: str = typer.Option( - '', - '--job-name', - help='按任务名称过滤', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_names, - ), - job_group: str = typer.Option('', '--job-group', help='按任务组过滤'), - status: str | None = typer.Option(None, '--status', help='按执行状态过滤,0成功 1失败'), - begin_date: str = typer.Option('', '--begin-date', help='按创建时间开始日期过滤,格式 YYYY-MM-DD'), - end_date: str = typer.Option('', '--end-date', help='按创建时间结束日期过滤,格式 YYYY-MM-DD'), - paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'), - page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'), - page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'), -) -> None: - """ - 查看定时任务执行日志列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 执行状态过滤条件 - :param begin_date: 查询开始日期 - :param end_date: 查询结束日期 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - _JOB_COMMAND_CONTROLLER.logs( - env, - output, - job_name=job_name, - job_group=job_group, - status=status, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - - -@app.command('pause', help='暂停定时任务') -def pause( - job_id: int = typer.Argument( - ..., - help='任务ID', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 暂停定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _JOB_COMMAND_CONTROLLER.pause(job_id, env, output, allow_prod, yes) - - -@app.command('resume', help='恢复定时任务') -def resume( - job_id: int = typer.Argument( - ..., - help='任务ID', - autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids, - ), - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 恢复定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _JOB_COMMAND_CONTROLLER.resume(job_id, env, output, allow_prod, yes) - - -@app.command('sync', help='同步调度任务配置') -def sync( - env: EnvOption = 'dev', - output: OutputOption = 'text', - allow_prod: AllowProdOption = False, - yes: YesOption = False, -) -> None: - """ - 同步调度任务配置。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - _JOB_COMMAND_CONTROLLER.sync(env, output, allow_prod, yes) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/job/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/job/controller.py deleted file mode 100644 index 92bc252..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/job/controller.py +++ /dev/null @@ -1,221 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.runtime.job import JOB_RUNTIME, JobRuntimeService -from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService - -from .presenter import JobCommandPresenter - - -class JobCommandController: - """ - 定时任务命令控制器。 - - 该控制器负责组织 `job` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 定时任务命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: JobCommandPresenter | None = None, - operations_runtime: OperationsRuntimeService | None = None, - job_runtime: JobRuntimeService | None = None, - ) -> None: - """ - 初始化定时任务命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 定时任务命令文本渲染器 - :param operations_runtime: 运维运行时服务 - :param job_runtime: 定时任务运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or JobCommandPresenter() - self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME - self.job_runtime = job_runtime or JOB_RUNTIME - - def list( - self, - env: str, - output: str, - *, - job_name: str, - job_group: str, - status: str | None, - paged: bool, - page_num: int, - page_size: int, - ) -> None: - """ - 查看定时任务列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 状态过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async( - self.job_runtime.list_jobs( - job_name=job_name, - job_group=job_group, - status=status, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - ) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_job_list_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def run_once(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None: - """ - 执行一次定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job run-once') - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.job_runtime.run_job_once(job_id)) - ) - - def detail(self, job_id: int, env: str, output: str) -> None: - """ - 查看单个定时任务详情。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.job_runtime.get_job_detail(job_id)) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_job_detail_text, - text_condition=lambda result_data: 'error' not in result_data, - ) - - def logs( - self, - env: str, - output: str, - *, - job_name: str, - job_group: str, - status: str | None, - begin_date: str, - end_date: str, - paged: bool, - page_num: int, - page_size: int, - ) -> None: - """ - 查看定时任务执行日志列表。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 执行状态过滤条件 - :param begin_date: 查询开始日期 - :param end_date: 查询结束日期 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async( - self.job_runtime.list_job_logs( - job_name=job_name, - job_group=job_group, - status=status, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - ) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_job_logs_text, - text_condition=lambda result_data: bool(result_data.get('ok', False)), - ) - - def pause(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None: - """ - 暂停定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job pause') - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.job_runtime.pause_job(job_id)) - ) - - def resume(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None: - """ - 恢复定时任务。 - - :param job_id: 任务ID - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job resume') - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.job_runtime.resume_job(job_id)) - ) - - def sync(self, env: str, output: str, allow_prod: bool, yes: bool) -> None: - """ - 同步调度任务配置。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境危险命令 - :param yes: 是否跳过确认 - :return: None - """ - ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job sync') - self.execution_service.complete_payload( - ctx, - self.execution_service.run_async(self.operations_runtime.sync_jobs()), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/job/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/job/presenter.py deleted file mode 100644 index e52f570..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/job/presenter.py +++ /dev/null @@ -1,197 +0,0 @@ -from cli.utils import SHELL_TEXT_FORMATTER - - -class JobCommandPresenter: - """ - 定时任务命令文本渲染器。 - - 该渲染器负责将 `job` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_job_list_text(self, payload: dict[str, object]) -> str: - """ - 将任务列表结果渲染为文本摘要。 - - :param payload: 任务列表结果字典 - :return: 文本摘要 - """ - lines = [f'ok: {str(payload.get("ok", False)).lower()}'] - - filters = payload.get('filters') - if isinstance(filters, dict): - lines.extend(self._build_job_filter_lines(filters)) - - page_payload = payload.get('page') - if isinstance(page_payload, dict): - rows = page_payload.get('rows', []) - lines.append( - 'page: ' - f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} ' - f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})' - ) - lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}') - if not isinstance(rows, list) or not rows: - lines.append('jobs: none') - return '\n'.join(lines) - lines.append('jobs:') - for row in rows: - if isinstance(row, dict): - lines.extend([f' {item}' for item in self._build_job_item_lines(row)]) - return '\n'.join(lines) - - items = payload.get('items') - lines.append(f'count: {payload.get("count", 0)}') - if not isinstance(items, list) or not items: - lines.append('jobs: none') - return '\n'.join(lines) - - lines.append('jobs:') - for item in items: - if isinstance(item, dict): - lines.extend([f' {line}' for line in self._build_job_item_lines(item)]) - return '\n'.join(lines) - - def build_job_detail_text(self, payload: dict[str, object]) -> str: - """ - 将单个定时任务详情渲染为文本摘要。 - - :param payload: 定时任务详情结果字典 - :return: 文本摘要 - """ - job = payload.get('job') - if not isinstance(job, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'job_id: {payload.get("jobId", "-")}', - 'job: none', - ] - ) - - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'job_id: {job.get("jobId", "-")}', - f'job_name: {job.get("jobName", "-")}', - f'job_group: {job.get("jobGroup", "-")}', - f'job_executor: {job.get("jobExecutor", "-")}', - f'status: {job.get("status", "-")}', - f'cron_expression: {job.get("cronExpression", "-")}', - f'invoke_target: {SHELL_TEXT_FORMATTER.truncate_text(job.get("invokeTarget", ""), 160) or "-"}', - f'job_args: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobArgs", ""), 120) or "-"}', - f'job_kwargs: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobKwargs", ""), 120) or "-"}', - f'misfire_policy: {job.get("misfirePolicy", "-")}', - f'concurrent: {job.get("concurrent", "-")}', - f'remark: {SHELL_TEXT_FORMATTER.truncate_text(job.get("remark", ""), 160) or "-"}', - f'create_by: {job.get("createBy", "-")}', - f'create_time: {job.get("createTime", "-")}', - f'update_by: {job.get("updateBy", "-")}', - f'update_time: {job.get("updateTime", "-")}', - ] - ) - - def build_job_logs_text(self, payload: dict[str, object]) -> str: - """ - 将定时任务日志列表结果渲染为文本摘要。 - - :param payload: 定时任务日志列表结果字典 - :return: 文本摘要 - """ - lines = [f'ok: {str(payload.get("ok", False)).lower()}'] - - filters = payload.get('filters') - if isinstance(filters, dict): - lines.extend(self._build_job_filter_lines(filters)) - - page_payload = payload.get('page') - if isinstance(page_payload, dict): - rows = page_payload.get('rows', []) - lines.append( - 'page: ' - f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} ' - f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})' - ) - lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}') - if not isinstance(rows, list) or not rows: - lines.append('logs: none') - return '\n'.join(lines) - lines.append('logs:') - for row in rows: - if isinstance(row, dict): - lines.extend([f' {item}' for item in self._build_job_log_item_lines(row)]) - return '\n'.join(lines) - - items = payload.get('items') - lines.append(f'count: {payload.get("count", 0)}') - if not isinstance(items, list) or not items: - lines.append('logs: none') - return '\n'.join(lines) - - lines.append('logs:') - for item in items: - if isinstance(item, dict): - lines.extend([f' {line}' for line in self._build_job_log_item_lines(item)]) - return '\n'.join(lines) - - @staticmethod - def _build_job_filter_lines(filters: dict[str, object]) -> list[str]: - """ - 构建任务列表过滤条件文本行。 - - :param filters: 过滤条件字典 - :return: 过滤条件文本行列表 - """ - active_filters = [] - for key, value in filters.items(): - if value in (None, '', False): - continue - active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}') - if not active_filters: - return ['filters: none'] - return ['filters:', *[f' - {item}' for item in active_filters]] - - @staticmethod - def _build_job_item_lines(job_item: dict[str, object]) -> list[str]: - """ - 构建单条任务记录的文本行。 - - :param job_item: 单条任务记录 - :return: 任务记录文本行列表 - """ - job_id = job_item.get('jobId', '-') - job_name = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobName', ''), 30) - job_group = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobGroup', ''), 16) - status = job_item.get('status', '-') - cron_expression = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('cronExpression', ''), 40) - invoke_target = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('invokeTarget', ''), 80) - executor = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobExecutor', ''), 24) - return [ - f'- [{job_id}] {job_name} | 组: {job_group} | 状态: {status}', - f' cron: {cron_expression or "-"}', - f' invoke: {invoke_target or "-"}', - f' executor: {executor or "-"}', - ] - - @staticmethod - def _build_job_log_item_lines(job_log_item: dict[str, object]) -> list[str]: - """ - 构建单条定时任务日志记录的文本行。 - - :param job_log_item: 单条定时任务日志记录 - :return: 文本行列表 - """ - job_log_id = job_log_item.get('jobLogId', '-') - job_name = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobName', ''), 30) - job_group = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobGroup', ''), 16) - status = job_log_item.get('status', '-') - job_trigger = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobTrigger', ''), 40) - job_message = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobMessage', ''), 80) - exception_info = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('exceptionInfo', ''), 120) - return [ - f'- [{job_log_id}] {job_name} | 组: {job_group} | 状态: {status}', - f' trigger: {job_trigger or "-"}', - f' message: {job_message or "-"}', - f' exception: {exception_info or "-"}', - f' create_time: {job_log_item.get("createTime", "-")}', - ] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/groups/ops/__init__.py deleted file mode 100644 index 0a2282e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .command import app - -__all__ = ['app'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/command.py b/shierfeng-fastapi-backend/build/lib/cli/groups/ops/command.py deleted file mode 100644 index c12530d..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/command.py +++ /dev/null @@ -1,89 +0,0 @@ -import typer - -from cli.context import EnvOption, OutputOption - -from .controller import OpsCommandController - -app = typer.Typer( - help='运维相关命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, -) -_OPS_COMMAND_CONTROLLER = OpsCommandController() - - -@app.command('ping-db', help='检查数据库连接') -def ping_db( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 检查数据库连接。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _OPS_COMMAND_CONTROLLER.ping_db(env, output) - - -@app.command('ping-redis', help='检查 Redis 连接') -def ping_redis_command( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 检查 Redis 连接。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _OPS_COMMAND_CONTROLLER.ping_redis(env, output) - - -@app.command('health', help='查看基础健康检查结果') -def health( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 输出基础健康检查结果。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _OPS_COMMAND_CONTROLLER.health(env, output) - - -@app.command('deps', help='查看当前 CLI 和后端运行依赖版本') -def deps( - env: EnvOption = 'dev', - output: OutputOption = 'text', - include_dev: bool = typer.Option(False, '--include-dev', help='附带输出开发依赖'), -) -> None: - """ - 检查当前 CLI 和后端运行依赖版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param include_dev: 是否附带输出开发依赖 - :return: None - """ - _OPS_COMMAND_CONTROLLER.deps(env, output, include_dev=include_dev) - - -@app.command('server-info', help='查看服务器运行时信息') -def server_info( - env: EnvOption = 'dev', - output: OutputOption = 'text', -) -> None: - """ - 输出服务器运行时信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - _OPS_COMMAND_CONTROLLER.server_info(env, output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/controller.py b/shierfeng-fastapi-backend/build/lib/cli/groups/ops/controller.py deleted file mode 100644 index 346abeb..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/controller.py +++ /dev/null @@ -1,137 +0,0 @@ -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliContextFactory, - CliExecutionService, -) -from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS -from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService -from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService - -from .presenter import OpsCommandPresenter - - -class OpsCommandController: - """ - 运维命令控制器。 - - 该控制器负责组织 `ops` 命令组的上下文准备、runtime 调用、 - payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 运维命令文本渲染器 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory | None = None, - execution_service: CliExecutionService | None = None, - presenter: OpsCommandPresenter | None = None, - database_runtime: DatabaseRuntimeService | None = None, - operations_runtime: OperationsRuntimeService | None = None, - ) -> None: - """ - 初始化运维命令控制器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param presenter: 运维命令文本渲染器 - :param database_runtime: 数据库运行时服务 - :param operations_runtime: 运维运行时服务 - :return: None - """ - self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory - self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service - self.presenter = presenter or OpsCommandPresenter() - self.database_runtime = database_runtime or DATABASE_RUNTIME - self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME - - def ping_db(self, env: str, output: str) -> None: - """ - 检查数据库连接。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload( - ctx, self.execution_service.run_async(self.database_runtime.ping_database()) - ) - - def ping_redis(self, env: str, output: str) -> None: - """ - 检查 Redis 连接。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - self.execution_service.complete_payload( - ctx, - self.execution_service.run_async(self.operations_runtime.ping_redis()), - ) - - def health(self, env: str, output: str) -> None: - """ - 输出基础健康检查结果。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - db_status = self.execution_service.run_async(self.database_runtime.ping_database()) - redis_status = self.execution_service.run_async(self.operations_runtime.ping_redis()) - payload = { - 'env': ctx.env, - 'database': db_status, - 'redis': redis_status, - 'ok': db_status.get('ok', False) and redis_status.get('ok', False), - } - exit_code = SUCCESS if payload['ok'] else DEPENDENCY_ERROR - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_health_text, - default_exit_code=exit_code, - ) - - def deps(self, env: str, output: str, *, include_dev: bool) -> None: - """ - 检查当前 CLI 和后端运行依赖版本。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :param include_dev: 是否附带输出开发依赖 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.operations_runtime.get_dependency_versions(include_dev=include_dev) - exit_code = SUCCESS if payload.get('ok', False) else DEPENDENCY_ERROR - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_dependencies_text, - default_exit_code=exit_code, - ) - - def server_info(self, env: str, output: str) -> None: - """ - 输出服务器运行时信息。 - - :param env: 当前命令运行环境 - :param output: 输出格式 - :return: None - """ - ctx = self.context_factory.build_readonly(env, output) - payload = self.execution_service.run_async(self.operations_runtime.get_server_info()) - self.execution_service.complete_payload_with_text( - ctx, - payload, - text_builder=self.presenter.build_server_info_text, - default_exit_code=SUCCESS, - text_condition=lambda data: data.get('ok', False), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/presenter.py b/shierfeng-fastapi-backend/build/lib/cli/groups/ops/presenter.py deleted file mode 100644 index 31aa1ab..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/groups/ops/presenter.py +++ /dev/null @@ -1,177 +0,0 @@ -from cli.utils import SHELL_TEXT_FORMATTER - - -class OpsCommandPresenter: - """ - 运维命令文本渲染器。 - - 该渲染器负责将 `ops` 命令组产生的结构化 payload 转换为稳定的文本摘要, - 同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。 - """ - - def build_health_text(self, payload: dict[str, object]) -> str: - """ - 将基础健康检查结果渲染为文本摘要。 - - :param payload: 健康检查结果字典 - :return: 文本摘要 - """ - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "")}', - 'checks:', - self._build_check_status_line('database', payload.get('database')), - self._build_check_status_line('redis', payload.get('redis')), - ] - ) - - def build_server_info_text(self, payload: dict[str, object]) -> str: - """ - 将服务器运行时信息渲染为文本摘要。 - - :param payload: 服务器运行时信息结果字典 - :return: 文本摘要 - """ - server = payload.get('server') - if not isinstance(server, dict): - return '\n'.join( - [ - f'ok: {str(payload.get("ok", False)).lower()}', - 'server: none', - ] - ) - - sys_info = server.get('sys') - cpu_info = server.get('cpu') - mem_info = server.get('mem') - py_info = server.get('py') - sys_files = server.get('sysFiles') - - lines = [f'ok: {str(payload.get("ok", False)).lower()}'] - if isinstance(sys_info, dict): - lines.extend( - [ - 'host:', - f' name: {sys_info.get("computerName", "-")}', - f' ip: {sys_info.get("computerIp", "-")}', - f' os: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("osName", "-"), 90)}', - f' arch: {sys_info.get("osArch", "-")}', - f' user_dir: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("userDir", "-"), 100)}', - ] - ) - - if isinstance(cpu_info, dict): - lines.extend( - [ - 'cpu:', - f' cores: {cpu_info.get("cpuNum", "-")}', - f' used: {cpu_info.get("used", "-")}%', - f' sys: {cpu_info.get("sys", "-")}%', - f' free: {cpu_info.get("free", "-")}%', - ] - ) - - if isinstance(mem_info, dict): - lines.extend( - [ - 'memory:', - f' total: {mem_info.get("total", "-")}', - f' used: {mem_info.get("used", "-")}', - f' free: {mem_info.get("free", "-")}', - f' usage: {mem_info.get("usage", "-")}%', - ] - ) - - if isinstance(py_info, dict): - lines.extend( - [ - 'python:', - f' name: {py_info.get("name", "-")}', - f' version: {py_info.get("version", "-")}', - f' start_time: {py_info.get("startTime", "-")}', - f' run_time: {py_info.get("runTime", "-")}', - f' home: {SHELL_TEXT_FORMATTER.truncate_text(py_info.get("home", "-"), 100)}', - f' process_memory: {py_info.get("used", "-")} / {py_info.get("total", "-")} ({py_info.get("usage", "-")}%)', - ] - ) - - if isinstance(sys_files, list): - lines.append(f'disks: {len(sys_files)}') - if sys_files: - lines.append('disk_samples:') - lines.extend( - f' - {item.get("dirName", "-")} | used: {item.get("used", "-")} / {item.get("total", "-")} | usage: {item.get("usage", "-")}' - for item in sys_files[:10] - if isinstance(item, dict) - ) - - return '\n'.join(lines) - - def build_dependencies_text(self, payload: dict[str, object]) -> str: - """ - 将依赖版本检查结果渲染为文本摘要。 - - :param payload: 依赖版本检查结果字典 - :return: 文本摘要 - """ - packages = payload.get('packages') - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'message: {payload.get("message", "-")}', - f'include_dev: {str(payload.get("includeDev", False)).lower()}', - ] - missing_required = payload.get('missingRequired') - if isinstance(missing_required, list) and missing_required: - lines.append('missing_required:') - lines.extend(f' - {item}' for item in missing_required) - else: - lines.append('missing_required: none') - - if not isinstance(packages, dict) or not packages: - lines.append('packages: none') - return '\n'.join(lines) - - lines.append('packages:') - for package_name in sorted(packages): - dependency_payload = packages.get(package_name) - if isinstance(dependency_payload, dict): - lines.append(self._build_dependency_line(package_name, dependency_payload)) - return '\n'.join(lines) - - @staticmethod - def _build_check_status_line(name: str, status_payload: dict[str, object] | None) -> str: - """ - 构建单项检查结果摘要行。 - - :param name: 检查项名称 - :param status_payload: 检查结果字典 - :return: 摘要行文本 - """ - if not isinstance(status_payload, dict): - return f' {name}: unknown' - - ok = str(status_payload.get('ok', False)).lower() - message = status_payload.get('message', '-') or '-' - error = status_payload.get('error') - if error: - return f' {name}: {ok} | {message} | error: {SHELL_TEXT_FORMATTER.truncate_text(error, 120)}' - return f' {name}: {ok} | {message}' - - @staticmethod - def _build_dependency_line(name: str, dependency_payload: dict[str, object] | None) -> str: - """ - 构建单个依赖项的文本摘要行。 - - :param name: 依赖项名称 - :param dependency_payload: 依赖项结果字典 - :return: 文本摘要行 - """ - if not isinstance(dependency_payload, dict): - return f' {name}: not-installed' - - installed = str(dependency_payload.get('installed', False)).lower() - version = dependency_payload.get('version', '-') or '-' - required = str(dependency_payload.get('required', False)).lower() - distribution = dependency_payload.get('distribution', '') or name - return f' {name}: {installed} | version: {version} | required: {required} | dist: {distribution}' diff --git a/shierfeng-fastapi-backend/build/lib/cli/guards.py b/shierfeng-fastapi-backend/build/lib/cli/guards.py deleted file mode 100644 index 8f1c708..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/guards.py +++ /dev/null @@ -1,210 +0,0 @@ -import sys -from dataclasses import dataclass, field -from typing import Literal - -import click -import typer - -from cli.context import CliContext -from cli.exit_codes import GUARD_REJECTED -from cli.output import CommandResult - -DangerousCommandRiskLevel = Literal['high', 'normal'] - - -@dataclass(frozen=True) -class DangerousCommandRule: - """ - 危险命令保护规则。 - - :param command_name: 命令唯一标识 - :param risk_level: 风险级别 - :param supports_dry_run: 是否支持演练执行 - """ - - command_name: str - risk_level: DangerousCommandRiskLevel - supports_dry_run: bool - - -@dataclass(frozen=True) -class DangerousCommandRuleRegistry: - """ - 危险命令规则注册表。 - - 该注册表负责维护 CLI 内所有危险命令的风险元数据,并提供查询与 - 强制获取能力,作为上下文工厂、风险元数据与测试的统一入口。 - - :param rules: 按命令名索引的危险命令规则表 - """ - - rules: dict[str, DangerousCommandRule] - - def get_rule(self, command_name: str) -> DangerousCommandRule | None: - """ - 获取指定命令的危险命令规则。 - - :param command_name: 命令唯一标识 - :return: 命令规则,不存在时返回 None - """ - return self.rules.get(command_name) - - def require_rule(self, command_name: str) -> DangerousCommandRule: - """ - 获取指定命令的危险命令规则,不存在时抛出异常。 - - :param command_name: 命令唯一标识 - :return: 命令规则 - :raises ValueError: 命令未注册危险命令规则时抛出 - """ - rule = self.get_rule(command_name) - if rule is None: - raise ValueError(f'危险命令未注册保护规则:{command_name}') - return rule - - -@dataclass(frozen=True) -class DangerousCommandResultBuilder: - """ - 危险命令结果构建器。 - - 该对象负责统一构建危险命令拒绝结果,避免保护服务内部继续拼装 - 结构化负载细节。 - """ - - @staticmethod - def build_guard_reject_result(message: str, hint: str) -> CommandResult: - """ - 构建危险命令拒绝结果。 - - :param message: 拒绝原因 - :param hint: 补充提示 - :return: 命令拒绝结果 - """ - return CommandResult( - data={ - 'ok': False, - 'message': message, - 'hint': hint, - }, - exit_code=GUARD_REJECTED, - ) - - -@dataclass(frozen=True) -class DangerousCommandConfirmationService: - """ - 危险命令确认服务。 - - 该对象负责处理 TTY 检测、确认提示与交互取消异常收口。 - - :param result_builder: 危险命令结果构建器 - """ - - result_builder: DangerousCommandResultBuilder - - def confirm(self, ctx: CliContext, *, command_name: str) -> CommandResult | None: - """ - 执行危险命令交互确认。 - - :param ctx: CLI 上下文 - :param command_name: 命令唯一标识 - :return: 拒绝结果或 None - """ - if ctx.yes: - return None - - if not sys.stdin.isatty(): - return self.result_builder.build_guard_reject_result( - f'已取消危险命令执行:{command_name}', - '当前命令需要交互确认;如需非交互执行,请传入 --yes', - ) - - try: - confirmed = typer.confirm( - f'确认执行危险命令 `{command_name}` 吗? 当前环境:{ctx.env}{"(dry-run)" if ctx.dry_run else ""}', - default=False, - ) - except (click.Abort, EOFError, KeyboardInterrupt): - return self.result_builder.build_guard_reject_result( - f'已取消危险命令执行:{command_name}', - '当前命令需要交互确认;如需非交互执行,请传入 --yes', - ) - if confirmed: - return None - - return self.result_builder.build_guard_reject_result( - f'已取消危险命令执行:{command_name}', - '如需跳过确认,请传入 --yes', - ) - - -@dataclass -class DangerousCommandGuardService: - """ - 危险命令保护执行服务。 - - 该服务负责根据命令规则和 CLI 上下文执行生产环境保护、交互确认 - 与拒绝结果收口。 - - :param rule_registry: 危险命令规则注册表 - :param result_builder: 危险命令结果构建器 - :param confirmation_service: 危险命令确认服务 - """ - - rule_registry: DangerousCommandRuleRegistry - result_builder: DangerousCommandResultBuilder = field(default_factory=DangerousCommandResultBuilder) - confirmation_service: DangerousCommandConfirmationService = field(init=False) - - def __post_init__(self) -> None: - """ - 初始化危险命令保护服务依赖。 - - :return: None - """ - self.confirmation_service = DangerousCommandConfirmationService(self.result_builder) - - def guard(self, ctx: CliContext, *, rule: DangerousCommandRule) -> CommandResult | None: - """ - 执行危险命令保护与确认。 - - :param ctx: CLI 上下文 - :param rule: 危险命令规则 - :return: 拒绝结果或 None - """ - command_name = rule.command_name - if ctx.env == 'prod' and not ctx.allow_prod: - return self.result_builder.build_guard_reject_result( - f'生产环境默认禁止直接执行危险命令:{command_name}', - '如确认执行,请传入 --allow-prod;如需跳过确认,请同时传入 --yes', - ) - - return self.confirmation_service.confirm(ctx, command_name=command_name) - - -DEFAULT_DANGEROUS_COMMAND_RULES: dict[str, DangerousCommandRule] = { - 'cache clear': DangerousCommandRule(command_name='cache clear', risk_level='high', supports_dry_run=True), - 'cache warmup': DangerousCommandRule(command_name='cache warmup', risk_level='normal', supports_dry_run=False), - 'db upgrade': DangerousCommandRule(command_name='db upgrade', risk_level='high', supports_dry_run=True), - 'db init': DangerousCommandRule(command_name='db init', risk_level='high', supports_dry_run=True), - 'db downgrade': DangerousCommandRule(command_name='db downgrade', risk_level='high', supports_dry_run=True), - 'db revision': DangerousCommandRule(command_name='db revision', risk_level='high', supports_dry_run=True), - 'config set': DangerousCommandRule(command_name='config set', risk_level='high', supports_dry_run=True), - 'config sync-cache': DangerousCommandRule( - command_name='config sync-cache', - risk_level='normal', - supports_dry_run=False, - ), - 'crypto rotate': DangerousCommandRule(command_name='crypto rotate', risk_level='high', supports_dry_run=True), - 'job run-once': DangerousCommandRule(command_name='job run-once', risk_level='normal', supports_dry_run=False), - 'job pause': DangerousCommandRule(command_name='job pause', risk_level='normal', supports_dry_run=False), - 'job resume': DangerousCommandRule(command_name='job resume', risk_level='normal', supports_dry_run=False), - 'job sync': DangerousCommandRule(command_name='job sync', risk_level='normal', supports_dry_run=False), - 'gen import-table': DangerousCommandRule(command_name='gen import-table', risk_level='high', supports_dry_run=True), - 'gen create-table': DangerousCommandRule(command_name='gen create-table', risk_level='high', supports_dry_run=True), - 'gen export': DangerousCommandRule(command_name='gen export', risk_level='high', supports_dry_run=True), - 'gen sync-db': DangerousCommandRule(command_name='gen sync-db', risk_level='normal', supports_dry_run=False), -} - -DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY = DangerousCommandRuleRegistry(rules=DEFAULT_DANGEROUS_COMMAND_RULES) -DEFAULT_DANGEROUS_COMMAND_GUARD = DangerousCommandGuardService(rule_registry=DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY) diff --git a/shierfeng-fastapi-backend/build/lib/cli/main.py b/shierfeng-fastapi-backend/build/lib/cli/main.py deleted file mode 100644 index 32c59bf..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/main.py +++ /dev/null @@ -1,100 +0,0 @@ -import sys -from dataclasses import dataclass - -from cli.core import ( - DEFAULT_CORE_SERVICES, - CliApplicationBuilder, - CompletionDispatcher, - ProjectRuntimeLocator, -) - - -@dataclass(frozen=True) -class CliImportArgvScope: - """ - CLI 导入期参数作用域。 - - 该对象负责在根应用构建阶段暂时切换 `sys.argv`,只保留导入期 - 需要的最小参数集,避免该过程继续以内联 `try/finally` 形式散落 - 在入口运行器中。 - - :param project_runtime_locator: CLI 项目运行时定位器 - """ - - project_runtime_locator: ProjectRuntimeLocator - - def run(self, callback: object) -> object: - """ - 在导入期参数作用域内执行回调。 - - :param callback: 待执行回调 - :return: 回调执行结果 - """ - original_argv = list(sys.argv) - try: - sys.argv = self.project_runtime_locator.extract_import_argv(original_argv) - return callback() - finally: - sys.argv = original_argv - - -@dataclass(frozen=True) -class CliMainRunner: - """ - CLI 根入口运行器。 - - 该运行器负责串联项目目录定位、导入期参数裁剪、根应用构建、 - completion 分发与 Typer 应用启动,避免 `main.py` 继续停留在 - 模块级散乱编排状态。 - - :param project_runtime_locator: CLI 项目运行时定位器 - :param completion_dispatcher: shell completion 分发器 - :param application_builder: 根应用构建器 - """ - - project_runtime_locator: ProjectRuntimeLocator - completion_dispatcher: CompletionDispatcher - application_builder: CliApplicationBuilder - import_argv_scope: CliImportArgvScope - - def build_cli(self) -> object: - """ - 构建 CLI 根应用,并在导入期仅保留最小化参数集。 - - :return: Typer 根应用 - """ - self.project_runtime_locator.ensure_backend_dir_on_sys_path() - return self.import_argv_scope.run(self.application_builder.build) - - def run(self) -> None: - """ - 执行 Typer CLI 根应用。 - - :return: None - """ - cli = self.build_cli() - self.completion_dispatcher.dispatch(cli) - cli(prog_name='shierfeng') - - -CLI_APPLICATION_BUILDER = CliApplicationBuilder(output_renderer=DEFAULT_CORE_SERVICES.output_renderer) -PROJECT_RUNTIME_LOCATOR = ProjectRuntimeLocator() -CLI_MAIN_RUNNER = CliMainRunner( - project_runtime_locator=PROJECT_RUNTIME_LOCATOR, - completion_dispatcher=CompletionDispatcher(), - application_builder=CLI_APPLICATION_BUILDER, - import_argv_scope=CliImportArgvScope(project_runtime_locator=PROJECT_RUNTIME_LOCATOR), -) - - -def main() -> None: - """ - 执行 CLI 根入口运行器。 - - :return: None - """ - CLI_MAIN_RUNNER.run() - - -if __name__ == '__main__': - main() diff --git a/shierfeng-fastapi-backend/build/lib/cli/metadata/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/metadata/__init__.py deleted file mode 100644 index 0d626c8..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/metadata/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from cli.metadata.command_specs import COMPLETION_SHELL_SPEC_REGISTRY, CompletionShellSpec, CompletionShellSpecRegistry -from cli.metadata.option_specs import ENVIRONMENT_OPTION_SERVICE, EnvironmentOptionService - -__all__ = [ - 'COMPLETION_SHELL_SPEC_REGISTRY', - 'ENVIRONMENT_OPTION_SERVICE', - 'CompletionShellSpec', - 'CompletionShellSpecRegistry', - 'EnvironmentOptionService', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/metadata/command_specs.py b/shierfeng-fastapi-backend/build/lib/cli/metadata/command_specs.py deleted file mode 100644 index 0b1fa5b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/metadata/command_specs.py +++ /dev/null @@ -1,98 +0,0 @@ -from dataclasses import dataclass -from typing import Literal - - -@dataclass(frozen=True) -class CompletionShellSpec: - """ - Shell completion 元数据定义。 - - name: shell 名称 - description: shell 描述文本 - generator: 脚本生成方式 - default_target: 默认脚本落盘路径,相对于用户 home 目录 - default_rc_file: 默认 rc 文件路径,相对于用户 home 目录 - auto_discovery: 是否会被 shell 自动发现 - supported: 当前版本是否已支持 - """ - - name: str - description: str - generator: Literal['click', 'custom', 'unsupported'] - default_target: str - default_rc_file: str | None - auto_discovery: bool - supported: bool - - -@dataclass(frozen=True) -class CompletionShellSpecRegistry: - """ - completion shell 元数据注册表。 - - 该注册表集中维护 CLI 当前支持的 shell completion 元数据, - 供 installer、doctor 和 provider 等场景统一查询。 - - :param specs: 按 shell 名称索引的元数据表 - """ - - specs: dict[str, CompletionShellSpec] - - def get_spec(self, shell_name: str) -> CompletionShellSpec | None: - """ - 获取指定 shell 的 completion 元数据。 - - :param shell_name: shell 名称 - :return: shell 元数据,不存在时返回 None - """ - return self.specs.get(shell_name) - - def list_shell_names(self) -> list[str]: - """ - 获取已注册的 shell 名称列表。 - - :return: shell 名称列表 - """ - return list(self.specs) - - -DEFAULT_COMPLETION_SHELL_SPECS: dict[str, CompletionShellSpec] = { - 'bash': CompletionShellSpec( - name='bash', - description='GNU Bash shell completion', - generator='click', - default_target='.local/share/shierfeng/completion/shierfeng.bash', - default_rc_file='.bashrc', - auto_discovery=False, - supported=True, - ), - 'zsh': CompletionShellSpec( - name='zsh', - description='Zsh shell completion', - generator='click', - default_target='.local/share/shierfeng/completion/shierfeng.zsh', - default_rc_file='.zshrc', - auto_discovery=False, - supported=True, - ), - 'fish': CompletionShellSpec( - name='fish', - description='Fish shell completion', - generator='click', - default_target='.config/fish/completions/shierfeng.fish', - default_rc_file=None, - auto_discovery=True, - supported=True, - ), - 'powershell': CompletionShellSpec( - name='powershell', - description='Windows PowerShell / PowerShell 7 completion', - generator='custom', - default_target='Documents/PowerShell/Profile/shierfeng.ps1', - default_rc_file='Documents/PowerShell/Microsoft.PowerShell_profile.ps1', - auto_discovery=False, - supported=True, - ), -} - -COMPLETION_SHELL_SPEC_REGISTRY = CompletionShellSpecRegistry(specs=DEFAULT_COMPLETION_SHELL_SPECS) diff --git a/shierfeng-fastapi-backend/build/lib/cli/metadata/option_specs.py b/shierfeng-fastapi-backend/build/lib/cli/metadata/option_specs.py deleted file mode 100644 index 0788701..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/metadata/option_specs.py +++ /dev/null @@ -1,36 +0,0 @@ -from dataclasses import dataclass -from pathlib import Path - -DEFAULT_ENVIRONMENTS = ('dev', 'prod', 'dockermy') - - -@dataclass(frozen=True) -class EnvironmentOptionService: - """ - CLI 环境选项服务。 - - 该服务负责发现当前项目下可用的环境名称集合,供 completion、 - wizard 和诊断信息等场景复用。 - - :param default_environments: 默认内置环境名称列表 - """ - - default_environments: tuple[str, ...] = DEFAULT_ENVIRONMENTS - - def discover_env_names(self, project_dir: Path | None = None) -> list[str]: - """ - 发现当前项目可用的环境名称列表。 - - :param project_dir: 后端项目目录,默认使用当前工作目录 - :return: 去重且排序后的环境名称列表 - """ - resolved_project_dir = (project_dir or Path.cwd()).resolve() - env_names = set(self.default_environments) - for env_file in resolved_project_dir.glob('.env.*'): - suffix = env_file.name.removeprefix('.env.').strip() - if suffix: - env_names.add(suffix) - return sorted(env_names) - - -ENVIRONMENT_OPTION_SERVICE = EnvironmentOptionService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/metadata/risk_specs.py b/shierfeng-fastapi-backend/build/lib/cli/metadata/risk_specs.py deleted file mode 100644 index d009304..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/metadata/risk_specs.py +++ /dev/null @@ -1,75 +0,0 @@ -from dataclasses import dataclass -from typing import Literal - -from cli.guards import DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY, DangerousCommandRuleRegistry - -CommandRiskLevel = Literal['readonly', 'normal', 'high'] - - -@dataclass(frozen=True) -class CommandRiskSpec: - """ - CLI 命令风险元数据定义。 - - :param command_name: 命令唯一标识 - :param risk_level: 风险级别 - :param supports_dry_run: 是否支持演练执行 - """ - - command_name: str - risk_level: CommandRiskLevel - supports_dry_run: bool - - -@dataclass(frozen=True) -class CommandRiskSpecRegistry: - """ - CLI 命令风险元数据注册表。 - - 该注册表负责从危险命令规则注册表派生风险元数据,供 TUI、文档、 - 诊断提示等只读场景统一查询。 - - :param specs: 按命令名索引的风险元数据表 - """ - - specs: dict[str, CommandRiskSpec] - - def get_spec(self, command_name: str) -> CommandRiskSpec | None: - """ - 获取指定命令的风险元数据。 - - :param command_name: 命令唯一标识 - :return: 风险元数据,不存在时返回 None - """ - return self.specs.get(command_name) - - -class CommandRiskSpecRegistryBuilder: - """ - CLI 命令风险元数据注册表构建器。 - - 该构建器负责基于危险命令规则注册表生成命令风险元数据注册表, - 避免模块导入阶段散落重复的字典推导逻辑。 - """ - - @staticmethod - def build(rule_registry: DangerousCommandRuleRegistry) -> CommandRiskSpecRegistry: - """ - 基于危险命令规则注册表构建风险元数据注册表。 - - :param rule_registry: 危险命令规则注册表 - :return: 风险元数据注册表 - """ - return CommandRiskSpecRegistry( - specs={ - command_name: CommandRiskSpec( - command_name=rule.command_name, - risk_level=rule.risk_level, - supports_dry_run=rule.supports_dry_run, - ) - for command_name, rule in rule_registry.rules.items() - } - ) - - -COMMAND_RISK_SPEC_REGISTRY = CommandRiskSpecRegistryBuilder.build(DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY) diff --git a/shierfeng-fastapi-backend/build/lib/cli/output.py b/shierfeng-fastapi-backend/build/lib/cli/output.py deleted file mode 100644 index 121ff17..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/output.py +++ /dev/null @@ -1,873 +0,0 @@ -import json -import os -import re -import sys -from dataclasses import dataclass -from typing import Any - -import typer - -from cli.context import CliContext - - -@dataclass -class OutputSettings: - """ - 输出渲染配置。 - - :param color_mode: 颜色模式 - :param icon_mode: 图标模式 - """ - - color_mode: str = 'always' - icon_mode: str = 'emoji' - - -@dataclass(frozen=True) -class StatusTokenDefinition: - """ - 状态标签定义。 - - :param emoji: emoji 模式文案 - :param ascii: ASCII 模式文案 - :param none: 无图标模式文案 - :param color: 默认前景色 - """ - - emoji: str - ascii: str - none: str - color: str - - -@dataclass -class CommandResult: - """ - 统一命令执行结果。 - - :param data: 命令结果负载 - :param exit_code: 命令退出码 - :param already_printed: 是否已由上层提前输出 - """ - - data: Any = None - exit_code: int = 0 - already_printed: bool = False - - -class OutputStatusStyler: - """ - 统一负责状态标签与单行文本样式。 - - :param settings: 输出配置 - """ - - _STATUS_DEFINITIONS = { - 'ok': StatusTokenDefinition(emoji='✅ OK', ascii='[OK]', none='OK', color='green'), - 'fail': StatusTokenDefinition(emoji='❌ FAIL', ascii='[FAIL]', none='FAIL', color='red'), - 'warn': StatusTokenDefinition(emoji='💡 HINT', ascii='[HINT]', none='HINT', color='yellow'), - 'error': StatusTokenDefinition(emoji='🚨 ERROR', ascii='[ERROR]', none='ERROR', color='red'), - 'info': StatusTokenDefinition(emoji='💬 INFO', ascii='[INFO]', none='INFO', color='blue'), - } - _STATUS_FIELD_KIND_MAPPING = { - 'message': 'info', - 'info': 'info', - 'hint': 'warn', - 'warn': 'warn', - 'warning': 'warn', - 'error': 'error', - } - - def __init__(self, settings: OutputSettings) -> None: - """ - 初始化状态样式器。 - - :param settings: 输出配置 - :return: None - """ - self.settings = settings - - def supports_color(self, stream: Any) -> bool: - """ - 判断当前输出流是否应启用颜色。 - - :param stream: 输出流对象 - :return: 是否启用颜色 - """ - if self.settings.color_mode == 'always': - return True - if self.settings.color_mode == 'never': - return False - if os.environ.get('NO_COLOR'): - return False - if os.environ.get('TERM') == 'dumb': - return False - return bool(getattr(stream, 'isatty', lambda: False)()) - - @staticmethod - def style_text(text: str, *, fg: str | None = None, bold: bool = False, dim: bool = False) -> str: - """ - 为文本附加终端样式。 - - :param text: 原始文本 - :param fg: 前景色 - :param bold: 是否加粗 - :param dim: 是否弱化显示 - :return: 样式化后的文本 - """ - return typer.style(text, fg=fg, bold=bold, dim=dim) - - def get_status_definition(self, kind: str) -> StatusTokenDefinition: - """ - 获取状态标签定义。 - - :param kind: 状态类型 - :return: 状态标签定义 - """ - return self._STATUS_DEFINITIONS.get(kind, self._STATUS_DEFINITIONS['info']) - - def build_status_token(self, kind: str) -> str: - """ - 根据当前图标模式构建状态标签文本。 - - :param kind: 标签类型 - :return: 标签文本 - """ - definition = self.get_status_definition(kind) - return getattr(definition, self.settings.icon_mode, definition.none) - - def style_status_token(self, kind: str, *, fg: str | None = None) -> str: - """ - 将状态标签渲染为高亮样式。 - - :param kind: 标签类型 - :param fg: 前景色 - :return: 样式化后的标签文本 - """ - definition = self.get_status_definition(kind) - return self.style_text(self.build_status_token(kind), fg=fg or definition.color, bold=True) - - def style_status_message(self, kind: str, text: str) -> str: - """ - 使用统一状态图标和颜色渲染消息文本。 - - :param kind: 状态类型 - :param text: 原始消息文本 - :return: 样式化后的消息文本 - """ - color = self.get_status_definition(kind).color - return f'{self.style_status_token(kind, fg=color)} {self.style_text(text, fg=color, bold=kind == "error")}' - - def build_result_header(self, is_ok: bool) -> str: - """ - 构建统一的结果头部文本。 - - :param is_ok: 是否成功 - :return: 结果头部文本 - """ - if is_ok: - return f'{self.build_status_token("ok")} SUCCESS' - return f'{self.build_status_token("fail")} FAILED' - - def style_scalar_text(self, value: str) -> str: - """ - 为标量文本值应用颜色样式。 - - :param value: 原始标量文本 - :return: 样式化后的文本 - """ - normalized_value = value.strip().lower() - if normalized_value == 'true': - return self.style_text(value, fg='green', bold=True) - if normalized_value == 'false': - return self.style_text(value, fg='red', bold=True) - if normalized_value in {'null', 'none', '-'}: - return self.style_text(value, fg='bright_black') - return value - - def style_inline_status_segment(self, segment: str) -> str: - """ - 为行内状态片段应用样式。 - - :param segment: 原始片段文本 - :return: 样式化后的片段文本 - """ - normalized_segment = segment.strip().lower() - if normalized_segment == 'true': - return self.style_status_token('ok', fg='green') - if normalized_segment == 'false': - return self.style_status_token('fail', fg='red') - for prefix, kind in self._STATUS_FIELD_KIND_MAPPING.items(): - if normalized_segment.startswith(f'{prefix}:'): - status_message = segment.split(':', 1)[1].strip() - return self.style_status_message(kind, status_message) - if ': ' in segment: - segment_key, segment_value = segment.split(': ', 1) - styled_segment_value = self.style_inline_status_value(segment_value) - if styled_segment_value != segment_value: - return f'{segment_key}: {styled_segment_value}' - return segment - - def style_inline_status_value(self, value: str) -> str: - """ - 为带分隔符的行内状态值应用样式。 - - :param value: 原始字段值 - :return: 样式化后的字段值 - """ - if ' | ' not in value: - styled_value = self.style_inline_status_segment(value) - if styled_value != value: - return styled_value - return self.style_scalar_text(value) - return ' | '.join(self.style_inline_status_segment(segment) for segment in value.split(' | ')) - - def style_named_value(self, key: str, value: str) -> str: - """ - 按字段语义为值应用样式。 - - :param key: 字段名 - :param value: 原始字段值 - :return: 样式化后的字段值 - """ - normalized_key = key.strip().lower() - normalized_value = value.strip().lower() - - if normalized_key == 'ok': - if normalized_value == 'true': - return self.style_status_token('ok', fg='green') - if normalized_value == 'false': - return self.style_status_token('fail', fg='red') - - status_kind = self._STATUS_FIELD_KIND_MAPPING.get(normalized_key) - if status_kind: - return self.style_status_message(status_kind, value) - - return self.style_inline_status_value(value) - - def style_line(self, line: str) -> str: - """ - 对单行文本输出应用可读性增强样式。 - - :param line: 原始输出行 - :return: 样式化后的输出行 - """ - if not line.strip(): - return line - - leading_spaces = len(line) - len(line.lstrip(' ')) - indent = line[:leading_spaces] - content = line[leading_spaces:] - - if content == self.build_result_header(True): - return ( - f'{indent}{self.style_status_token("ok", fg="green")} ' - f'{self.style_text("SUCCESS", fg="green", bold=True)}' - ) - if content == self.build_result_header(False): - return ( - f'{indent}{self.style_status_token("fail", fg="red")} {self.style_text("FAILED", fg="red", bold=True)}' - ) - - if content.startswith('- '): - return ( - f'{indent}{self.style_text("-", fg="bright_black", bold=True)} ' - f'{self.style_inline_status_value(content[2:])}' - ) - - if ': ' in content: - key, value = content.split(': ', 1) - styled_key = self.style_text(f'{key}:', fg='cyan', bold=leading_spaces == 0) - return f'{indent}{styled_key} {self.style_named_value(key, value)}' - - if content.endswith(':'): - return f'{indent}{self.style_text(content, fg="blue", bold=True)}' - - if content == '|': - return f'{indent}{self.style_text(content, fg="bright_black")}' - - return line - - def render_error_text(self, message: str, exit_code: int, stream: Any) -> str: - """ - 渲染标准错误输出文本。 - - :param message: 错误消息 - :param exit_code: 退出码 - :param stream: 输出流对象 - :return: 渲染后的错误文本 - """ - error_prefix = f'{self.build_status_token("error")}[{exit_code}]:' - if not self.supports_color(stream): - return f'{error_prefix} {message}' - return ( - f'{self.style_status_token("error", fg="red")}[{exit_code}]: ' - f'{self.style_text(message, fg="red", bold=True)}' - ) - - -class StructuredTextRenderer: - """ - 负责结构化数据到文本的转换与装饰。 - - :param status_styler: 状态样式器 - """ - - _SNAKE_CASE_BOUNDARY_PATTERN = re.compile(r'(? None: - """ - 初始化结构化文本渲染器。 - - :param status_styler: 状态样式器 - :return: None - """ - self.status_styler = status_styler - - def render_text_output(self, data: Any) -> str: - """ - 将任意数据渲染为纯文本输出。 - - :param data: 待渲染数据 - :return: 文本输出结果 - """ - if isinstance(data, str): - return data - return '\n'.join(self.render_text_lines(data)) - - def colorize_text_output(self, text: str, stream: Any) -> str: - """ - 按终端能力为文本输出增加颜色。 - - :param text: 原始文本输出 - :param stream: 输出流对象 - :return: 样式化后的文本输出 - """ - if not self.status_styler.supports_color(stream): - return text - return '\n'.join(self.status_styler.style_line(line) for line in text.splitlines()) - - def decorate_text_output(self, text: str) -> str: - """ - 对文本输出进行结构化装饰。 - - :param text: 原始文本输出 - :return: 装饰后的文本输出 - """ - lines = text.splitlines() - if not lines: - return text - - first_non_empty_index = next((index for index, line in enumerate(lines) if line.strip()), None) - if first_non_empty_index is None: - return text - - first_line = lines[first_non_empty_index].strip().lower() - if first_line == 'ok: true': - decorated_lines = list(lines) - decorated_lines[first_non_empty_index] = self.status_styler.build_result_header(True) - return '\n'.join(decorated_lines) - if first_line == 'ok: false': - decorated_lines = list(lines) - decorated_lines[first_non_empty_index] = self.status_styler.build_result_header(False) - return '\n'.join(decorated_lines) - return text - - @staticmethod - def format_scalar(value: Any) -> str: - """ - 将标量值格式化为文本输出。 - - :param value: 待格式化的值 - :return: 文本格式化结果 - """ - if value is None: - return 'null' - if isinstance(value, bool): - return 'true' if value else 'false' - return str(value) - - @classmethod - def format_field_name(cls, key: object) -> str: - """ - 将字段名标准化为文本输出使用的 `snake_case` 形式。 - - :param key: 原始字段名 - :return: 标准化后的字段名 - """ - text = str(key).strip() - if not text: - return '' - normalized_text = text.replace('-', '_').replace(' ', '_') - return cls._SNAKE_CASE_BOUNDARY_PATTERN.sub('_', normalized_text).lower() - - def append_multiline_text(self, lines: list[str], prefix: str, value: str, indent: str) -> None: - """ - 将多行文本追加到输出行列表中。 - - :param lines: 输出行列表 - :param prefix: 当前字段前缀 - :param value: 多行文本内容 - :param indent: 子级缩进 - :return: None - """ - lines.append(f'{prefix} |') - lines.extend(f'{indent}{line}' for line in value.splitlines()) - - def render_nested_lines(self, prefix: str, nested_data: dict[str, Any] | list[Any], indent_level: int) -> list[str]: - """ - 渲染嵌套字典或列表字段。 - - :param prefix: 当前字段前缀 - :param nested_data: 嵌套数据 - :param indent_level: 当前缩进层级 - :return: 渲染后的文本行列表 - """ - nested_lines = self.render_text_lines(nested_data, indent_level=indent_level + 1) - if len(nested_lines) == 1 and nested_lines[0].strip() in {'{}', '[]'}: - return [f'{prefix} {nested_lines[0].strip()}'] - return [prefix, *nested_lines] - - def render_mapping_lines(self, data: dict[str, Any], *, indent_level: int) -> list[str]: - """ - 将字典渲染为层级化文本输出。 - - :param data: 待渲染字典 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - indent = ' ' * indent_level - child_indent = ' ' * (indent_level + 1) - if not data: - return [f'{indent}{{}}'] - - lines: list[str] = [] - for key, value in data.items(): - field_name = self.format_field_name(key) - prefix = f'{indent}{field_name}:' - if isinstance(value, str) and '\n' in value: - self.append_multiline_text(lines, prefix, value, child_indent) - continue - if isinstance(value, dict | list): - lines.extend(self.render_nested_lines(prefix, value, indent_level)) - continue - lines.append(f'{prefix} {self.format_scalar(value)}') - return lines - - def render_list_lines(self, data: list[Any], *, indent_level: int) -> list[str]: - """ - 将列表渲染为层级化文本输出。 - - :param data: 待渲染列表 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - indent = ' ' * indent_level - child_indent = ' ' * (indent_level + 1) - if not data: - return [f'{indent}[]'] - - lines: list[str] = [] - for item in data: - item_prefix = f'{indent}-' - if isinstance(item, str) and '\n' in item: - self.append_multiline_text(lines, item_prefix, item, child_indent) - continue - if isinstance(item, dict | list): - lines.extend(self.render_nested_lines(item_prefix, item, indent_level)) - continue - lines.append(f'{item_prefix} {self.format_scalar(item)}') - return lines - - def render_text_lines(self, data: Any, *, indent_level: int = 0) -> list[str]: - """ - 将任意数据结构渲染为层级化文本输出。 - - :param data: 待渲染数据 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - if isinstance(data, dict): - return self.render_mapping_lines(data, indent_level=indent_level) - - if isinstance(data, list): - return self.render_list_lines(data, indent_level=indent_level) - - indent = ' ' * indent_level - if isinstance(data, str) and '\n' in data: - return [f'{indent}{line}' for line in data.splitlines()] - - return [f'{indent}{self.format_scalar(data)}'] - - -class OutputEmitter: - """ - 负责标准输出、标准错误与命令退出收口。 - - :param text_renderer: 结构化文本渲染器 - :param status_styler: 状态样式器 - """ - - def __init__(self, text_renderer: StructuredTextRenderer, status_styler: OutputStatusStyler) -> None: - """ - 初始化输出发射器。 - - :param text_renderer: 结构化文本渲染器 - :param status_styler: 状态样式器 - :return: None - """ - self.text_renderer = text_renderer - self.status_styler = status_styler - - def emit_output(self, data: Any, output_format: str) -> None: - """ - 输出命令结果。 - - :param data: 输出数据 - :param output_format: 输出格式 - :return: None - """ - if data is None: - return - if output_format == 'json': - print(json.dumps(data, ensure_ascii=False, indent=2, default=str)) - return - rendered_text = self.text_renderer.decorate_text_output(self.text_renderer.render_text_output(data)) - print(self.text_renderer.colorize_text_output(rendered_text, sys.stdout)) - - def emit_error(self, message: str, output_format: str, *, exit_code: int) -> None: - """ - 输出错误信息。 - - :param message: 错误消息 - :param output_format: 输出格式 - :param exit_code: 退出码 - :return: None - """ - if output_format == 'json': - print( - json.dumps({'ok': False, 'error': message, 'exitCode': exit_code}, ensure_ascii=False), - file=sys.stderr, - ) - return - print(self.status_styler.render_error_text(message, exit_code, sys.stderr), file=sys.stderr) - - def complete_command(self, result: CommandResult, ctx: CliContext) -> None: - """ - 输出命令结果并结束当前命令。 - - :param result: 命令执行结果 - :param ctx: CLI 上下文 - :return: None - :raises typer.Exit: 始终通过 Typer 退出并返回对应退出码 - """ - if not result.already_printed: - self.emit_output(result.data, ctx.output) - raise typer.Exit(code=result.exit_code) - - -class OutputRenderer: - """ - CLI 输出渲染门面。 - - :param color_mode: 颜色模式 - :param icon_mode: 图标模式 - """ - - def __init__(self, *, color_mode: str = 'always', icon_mode: str = 'emoji') -> None: - """ - 初始化输出渲染器。 - - :param color_mode: 颜色模式,支持 `auto`、`always`、`never` - :param icon_mode: 图标模式,支持 `emoji`、`ascii`、`none` - :return: None - """ - self.settings = OutputSettings(color_mode=color_mode, icon_mode=icon_mode) - self.status_styler = OutputStatusStyler(self.settings) - self.text_renderer = StructuredTextRenderer(self.status_styler) - self.emitter = OutputEmitter(self.text_renderer, self.status_styler) - - @property - def color_mode(self) -> str: - """ - 返回当前颜色模式。 - - :return: 颜色模式 - """ - return self.settings.color_mode - - @property - def icon_mode(self) -> str: - """ - 返回当前图标模式。 - - :return: 图标模式 - """ - return self.settings.icon_mode - - def set_color_mode(self, color_mode: str) -> None: - """ - 设置颜色模式。 - - :param color_mode: 颜色模式 - :return: None - """ - self.settings.color_mode = color_mode - - def set_icon_mode(self, icon_mode: str) -> None: - """ - 设置图标模式。 - - :param icon_mode: 图标模式 - :return: None - """ - self.settings.icon_mode = icon_mode - - def supports_color(self, stream: Any) -> bool: - """ - 判断当前输出流是否应启用颜色。 - - :param stream: 输出流对象 - :return: 是否启用颜色 - """ - return self.status_styler.supports_color(stream) - - @staticmethod - def style_text(text: str, *, fg: str | None = None, bold: bool = False, dim: bool = False) -> str: - """ - 为文本附加终端样式。 - - :param text: 原始文本 - :param fg: 前景色 - :param bold: 是否加粗 - :param dim: 是否弱化显示 - :return: 样式化后的文本 - """ - return OutputStatusStyler.style_text(text, fg=fg, bold=bold, dim=dim) - - def build_status_token(self, kind: str) -> str: - """ - 根据当前图标模式构建状态标签文本。 - - :param kind: 标签类型 - :return: 标签文本 - """ - return self.status_styler.build_status_token(kind) - - def style_status_token(self, kind: str, *, fg: str) -> str: - """ - 将状态标签渲染为高亮样式。 - - :param kind: 标签类型 - :param fg: 前景色 - :return: 样式化后的标签文本 - """ - return self.status_styler.style_status_token(kind, fg=fg) - - def style_status_message(self, kind: str, text: str) -> str: - """ - 使用统一状态图标和颜色渲染消息文本。 - - :param kind: 状态类型 - :param text: 原始消息文本 - :return: 样式化后的消息文本 - """ - return self.status_styler.style_status_message(kind, text) - - def build_result_header(self, is_ok: bool) -> str: - """ - 构建统一的结果头部文本。 - - :param is_ok: 是否成功 - :return: 结果头部文本 - """ - return self.status_styler.build_result_header(is_ok) - - def style_scalar_text(self, value: str) -> str: - """ - 为标量文本值应用颜色样式。 - - :param value: 原始标量文本 - :return: 样式化后的文本 - """ - return self.status_styler.style_scalar_text(value) - - def style_inline_status_segment(self, segment: str) -> str: - """ - 为行内状态片段应用样式。 - - :param segment: 原始片段文本 - :return: 样式化后的片段文本 - """ - return self.status_styler.style_inline_status_segment(segment) - - def style_inline_status_value(self, value: str) -> str: - """ - 为带分隔符的行内状态值应用样式。 - - :param value: 原始字段值 - :return: 样式化后的字段值 - """ - return self.status_styler.style_inline_status_value(value) - - def style_named_value(self, key: str, value: str) -> str: - """ - 按字段语义为值应用样式。 - - :param key: 字段名 - :param value: 原始字段值 - :return: 样式化后的字段值 - """ - return self.status_styler.style_named_value(key, value) - - def style_line(self, line: str) -> str: - """ - 对单行文本输出应用可读性增强样式。 - - :param line: 原始输出行 - :return: 样式化后的输出行 - """ - return self.status_styler.style_line(line) - - def render_text_output(self, data: Any) -> str: - """ - 将任意数据渲染为纯文本输出。 - - :param data: 待渲染数据 - :return: 文本输出结果 - """ - return self.text_renderer.render_text_output(data) - - def colorize_text_output(self, text: str, stream: Any) -> str: - """ - 按终端能力为文本输出增加颜色。 - - :param text: 原始文本输出 - :param stream: 输出流对象 - :return: 样式化后的文本输出 - """ - return self.text_renderer.colorize_text_output(text, stream) - - def decorate_text_output(self, text: str) -> str: - """ - 对文本输出进行结构化装饰。 - - :param text: 原始文本输出 - :return: 装饰后的文本输出 - """ - return self.text_renderer.decorate_text_output(text) - - @staticmethod - def format_scalar(value: Any) -> str: - """ - 将标量值格式化为文本输出。 - - :param value: 待格式化的值 - :return: 文本格式化结果 - """ - return StructuredTextRenderer.format_scalar(value) - - @staticmethod - def format_field_name(key: object) -> str: - """ - 将字段名标准化为文本输出使用的 `snake_case` 形式。 - - :param key: 原始字段名 - :return: 标准化后的字段名 - """ - return StructuredTextRenderer.format_field_name(key) - - def append_multiline_text(self, lines: list[str], prefix: str, value: str, indent: str) -> None: - """ - 将多行文本追加到输出行列表中。 - - :param lines: 输出行列表 - :param prefix: 当前字段前缀 - :param value: 多行文本内容 - :param indent: 子级缩进 - :return: None - """ - self.text_renderer.append_multiline_text(lines, prefix, value, indent) - - def render_nested_lines(self, prefix: str, nested_data: dict[str, Any] | list[Any], indent_level: int) -> list[str]: - """ - 渲染嵌套字典或列表字段。 - - :param prefix: 当前字段前缀 - :param nested_data: 嵌套数据 - :param indent_level: 当前缩进层级 - :return: 渲染后的文本行列表 - """ - return self.text_renderer.render_nested_lines(prefix, nested_data, indent_level) - - def render_mapping_lines(self, data: dict[str, Any], *, indent_level: int) -> list[str]: - """ - 将字典渲染为层级化文本输出。 - - :param data: 待渲染字典 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - return self.text_renderer.render_mapping_lines(data, indent_level=indent_level) - - def render_list_lines(self, data: list[Any], *, indent_level: int) -> list[str]: - """ - 将列表渲染为层级化文本输出。 - - :param data: 待渲染列表 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - return self.text_renderer.render_list_lines(data, indent_level=indent_level) - - def render_text_lines(self, data: Any, *, indent_level: int = 0) -> list[str]: - """ - 将任意数据结构渲染为层级化文本输出。 - - :param data: 待渲染数据 - :param indent_level: 当前缩进层级 - :return: 文本输出行列表 - """ - return self.text_renderer.render_text_lines(data, indent_level=indent_level) - - def emit_output(self, data: Any, output_format: str) -> None: - """ - 输出命令结果。 - - :param data: 输出数据 - :param output_format: 输出格式 - :return: None - """ - self.emitter.emit_output(data, output_format) - - def render_error_text(self, message: str, exit_code: int, stream: Any) -> str: - """ - 渲染标准错误输出文本。 - - :param message: 错误消息 - :param exit_code: 退出码 - :param stream: 输出流对象 - :return: 渲染后的错误文本 - """ - return self.status_styler.render_error_text(message, exit_code, stream) - - def emit_error(self, message: str, output_format: str, *, exit_code: int) -> None: - """ - 输出错误信息。 - - :param message: 错误消息 - :param output_format: 输出格式 - :param exit_code: 退出码 - :return: None - """ - self.emitter.emit_error(message, output_format, exit_code=exit_code) - - def complete_command(self, result: CommandResult, ctx: CliContext) -> None: - """ - 输出命令结果并结束当前命令。 - - :param result: 命令执行结果 - :param ctx: CLI 上下文 - :return: None - """ - self.emitter.complete_command(result, ctx) diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/__init__.py deleted file mode 100644 index efb1257..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService - -__all__ = ['RUNTIME_ENVIRONMENT', 'RuntimeEnvironmentService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/app/__init__.py deleted file mode 100644 index 47ddfc7..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import APP_RUNTIME, AppRuntimeService - -__all__ = ['APP_RUNTIME', 'AppRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/app/gateway.py deleted file mode 100644 index 2c46c3a..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/gateway.py +++ /dev/null @@ -1,29 +0,0 @@ -from importlib import import_module -from types import ModuleType - - -class AppInfrastructureGateway: - """ - 应用基础设施网关。 - - 该对象负责延迟加载应用实例构建和环境配置模块, - 供应用运行时 facade 与其协作对象统一复用。 - """ - - @staticmethod - def get_server_module() -> ModuleType: - """ - 获取应用 server 模块。 - - :return: server 模块 - """ - return import_module('server') - - @staticmethod - def get_env_module() -> ModuleType: - """ - 获取环境配置模块。 - - :return: 环境配置模块 - """ - return import_module('config.env') diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/app/service.py deleted file mode 100644 index 5cf5225..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/service.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Any - -from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService - -from .gateway import AppInfrastructureGateway -from .support import AppSnapshotSupport - - -class AppRuntimeService: - """ - 应用运行时服务。 - - 该服务作为应用运行时 facade,对外统一暴露应用实例构建、 - 应用配置快照与环境信息快照入口。 - - :param runtime_environment: 运行时环境服务 - :param infrastructure_gateway: 应用基础设施网关 - :param snapshot_support: 应用快照支持对象 - """ - - def __init__( - self, - *, - runtime_environment: RuntimeEnvironmentService | None = None, - infrastructure_gateway: AppInfrastructureGateway | None = None, - snapshot_support: AppSnapshotSupport | None = None, - ) -> None: - """ - 初始化应用运行时服务。 - - :param runtime_environment: 运行时环境服务 - :param infrastructure_gateway: 应用基础设施网关 - :param snapshot_support: 应用快照支持对象 - :return: None - """ - self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT - self.infrastructure_gateway = infrastructure_gateway or AppInfrastructureGateway() - self.snapshot_support = snapshot_support or AppSnapshotSupport( - self.infrastructure_gateway, - self.runtime_environment, - ) - - def build_app_instance(self) -> Any: - """ - 构建当前环境下的 FastAPI 应用实例。 - - :return: FastAPI 应用实例 - """ - server_module = self.infrastructure_gateway.get_server_module() - return server_module.create_app() - - def get_app_config_snapshot(self) -> dict[str, Any]: - """ - 读取当前运行环境的应用配置快照。 - - :return: 应用配置快照 - """ - return self.snapshot_support.build_app_config_snapshot() - - def get_app_env_snapshot(self) -> dict[str, Any]: - """ - 读取当前 CLI 进程的环境解析结果快照。 - - :return: 环境解析结果快照 - """ - return self.snapshot_support.build_app_env_snapshot() - - -APP_RUNTIME = AppRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/app/support.py deleted file mode 100644 index 169054c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/app/support.py +++ /dev/null @@ -1,90 +0,0 @@ -import os -from pathlib import Path -from typing import Any - -from cli.runtime.base import RuntimeEnvironmentService - -from .gateway import AppInfrastructureGateway - - -class AppSnapshotSupport: - """ - 应用快照支持对象。 - - 该对象负责应用配置快照和环境快照的构建, - 避免主运行时服务继续承载快照字段拼装细节。 - - :param infrastructure_gateway: 应用基础设施网关 - :param runtime_environment: 运行时环境服务 - """ - - def __init__( - self, - infrastructure_gateway: AppInfrastructureGateway, - runtime_environment: RuntimeEnvironmentService, - ) -> None: - """ - 初始化应用快照支持对象。 - - :param infrastructure_gateway: 应用基础设施网关 - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - self.runtime_environment = runtime_environment - - def build_app_config_snapshot(self) -> dict[str, Any]: - """ - 读取当前运行环境的应用配置快照。 - - :return: 应用配置快照 - """ - env_module = self.infrastructure_gateway.get_env_module() - app_config = env_module.AppConfig - database_config = env_module.DataBaseConfig - log_config = env_module.LogConfig - redis_config = env_module.RedisConfig - transport_crypto_config = env_module.TransportCryptoConfig - return { - 'env': app_config.app_env, - 'name': app_config.app_name, - 'host': app_config.app_host, - 'port': app_config.app_port, - 'rootPath': app_config.app_root_path, - 'reload': app_config.app_reload, - 'workers': app_config.app_workers, - 'disableSwagger': app_config.app_disable_swagger, - 'disableRedoc': app_config.app_disable_redoc, - 'dbType': database_config.db_type, - 'dbHost': database_config.db_host, - 'dbPort': database_config.db_port, - 'dbDatabase': database_config.db_database, - 'redisHost': redis_config.redis_host, - 'redisPort': redis_config.redis_port, - 'logLevel': log_config.loguru_level, - 'transportCryptoEnabled': transport_crypto_config.transport_crypto_enabled, - 'transportCryptoMode': transport_crypto_config.transport_crypto_mode, - } - - def build_app_env_snapshot(self) -> dict[str, Any]: - """ - 读取当前 CLI 进程的环境解析结果快照。 - - :return: 环境解析结果快照 - """ - env_module = self.infrastructure_gateway.get_env_module() - app_config = env_module.AppConfig - backend_dir = Path(self.runtime_environment.get_backend_dir()) - resolved_env = os.environ.get('APP_ENV', '') or 'dev' - env_file_name = f'.env.{resolved_env}' - env_file_path = backend_dir / env_file_name - return { - 'cliEnv': resolved_env, - 'configEnv': app_config.app_env, - 'appEnv': os.environ.get('APP_ENV', ''), - 'envFile': env_file_name, - 'envFilePath': str(env_file_path), - 'envFileExists': env_file_path.exists(), - 'backendDir': str(backend_dir), - 'pythonExecutable': self.runtime_environment.get_python_executable(), - } diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/base.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/base.py deleted file mode 100644 index c9b37f5..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/base.py +++ /dev/null @@ -1,72 +0,0 @@ -import getpass -import os -import sys -from pathlib import Path - - -class RuntimeEnvironmentService: - """ - CLI 运行时环境服务。 - - 该服务集中管理后端根目录解析、Python 解释器定位等稳定运行时能力, - 避免调用方继续依赖零散模块级函数。 - """ - - @staticmethod - def is_backend_project_dir(project_dir: Path) -> bool: - """ - 判断目录是否为后端项目根目录。 - - :param project_dir: 待检查目录 - :return: 是否为后端项目根目录 - """ - return ( - (project_dir / 'app.py').exists() - and (project_dir / 'config' / 'env.py').exists() - and (project_dir / 'cli').is_dir() - ) - - def get_backend_dir(self) -> str: - """ - 获取后端根目录绝对路径。 - - :return: 后端根目录绝对路径 - """ - current_dir = Path.cwd().resolve() - if self.is_backend_project_dir(current_dir): - return str(current_dir) - return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - @staticmethod - def get_python_executable() -> str: - """ - 获取当前 CLI 进程使用的 Python 解释器。 - - :return: Python 解释器路径或命令名 - """ - return sys.executable or 'python' - - -class RuntimeOperatorService: - """ - CLI 运行时操作者解析服务。 - - 该服务集中提供当前 CLI 进程的操作者名称解析能力, - 避免各领域运行时对象继续复制相同的 `getpass.getuser()` 回退逻辑。 - """ - - @staticmethod - def resolve_operator() -> str: - """ - 解析当前 CLI 操作者名称。 - - :return: 操作者名称 - """ - try: - return getpass.getuser() or 'shierfeng-cli' - except Exception: - return 'shierfeng-cli' - - -RUNTIME_ENVIRONMENT = RuntimeEnvironmentService() -RUNTIME_OPERATOR = RuntimeOperatorService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/__init__.py deleted file mode 100644 index 2b6dbb9..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import CACHE_RUNTIME, CacheRuntimeService - -__all__ = ['CACHE_RUNTIME', 'CacheRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/gateway.py deleted file mode 100644 index 9dd40a1..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/gateway.py +++ /dev/null @@ -1,41 +0,0 @@ -from importlib import import_module -from typing import Any - -REDIS_TTL_KEY_NOT_FOUND = -2 -REDIS_TTL_PERSISTENT = -1 - - -class CacheInfrastructureGateway: - """ - 缓存基础设施网关。 - - 该对象负责延迟加载 Redis 相关基础设施依赖, - 供缓存运行时 facade 和其协作对象统一复用。 - """ - - @staticmethod - def get_redis_error_class() -> type[Exception]: - """ - 获取 Redis 异常类型。 - - :return: Redis 异常类型 - """ - return import_module('redis.exceptions').RedisError - - @staticmethod - def get_redis_init_key_config() -> Any: - """ - 获取系统缓存初始化键枚举。 - - :return: 缓存初始化键枚举 - """ - return import_module('common.enums').RedisInitKeyConfig - - @staticmethod - def get_redis_util() -> Any: - """ - 获取 Redis 工具类。 - - :return: Redis 工具类 - """ - return import_module('config.get_redis').RedisUtil diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/service.py deleted file mode 100644 index b6a9cf7..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/service.py +++ /dev/null @@ -1,216 +0,0 @@ -from typing import Any - -from cli.exit_codes import RUNTIME_ERROR - -from .gateway import REDIS_TTL_KEY_NOT_FOUND, REDIS_TTL_PERSISTENT, CacheInfrastructureGateway -from .support import CacheDomainSupport, CacheRedisSupport - - -class CacheRuntimeService: - """ - 缓存运行时服务。 - - 该服务作为缓存运行时 facade,对外统一暴露缓存统计、键列表、键值、 - TTL 读取,以及缓存清理和预热入口。 - - :param infrastructure_gateway: 缓存基础设施网关 - :param domain_support: 缓存领域支持对象 - :param redis_support: 缓存 Redis 访问支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: CacheInfrastructureGateway | None = None, - domain_support: CacheDomainSupport | None = None, - redis_support: CacheRedisSupport | None = None, - ) -> None: - """ - 初始化缓存运行时服务。 - - :param infrastructure_gateway: 缓存基础设施网关 - :param domain_support: 缓存领域支持对象 - :param redis_support: 缓存 Redis 访问支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or CacheInfrastructureGateway() - self.domain_support = domain_support or CacheDomainSupport(self.infrastructure_gateway) - self.redis_support = redis_support or CacheRedisSupport(self.infrastructure_gateway, self.domain_support) - - async def get_cache_stats(self) -> dict[str, Any]: - """ - 获取缓存统计信息。 - - :return: 缓存统计结果 - """ - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with self.redis_support.redis_session() as (redis, _redis_util): - info = await redis.info() - db_size = await redis.dbsize() - command_stats_dict = await redis.info('commandstats') - command_stats = [ - {'name': key.split('_', 1)[1], 'value': int(value.get('calls', 0))} - for key, value in sorted(command_stats_dict.items(), key=lambda item: item[0]) - ] - return { - 'ok': True, - 'dbSize': db_size, - 'commandStats': command_stats, - 'cacheNames': self.domain_support.build_cache_name_items(), - 'info': info, - } - except redis_error as exc: - return self.redis_support.build_redis_error_result('读取缓存统计失败', exc) - - async def list_cache_keys(self, cache_name: str) -> dict[str, Any]: - """ - 列出指定缓存名称下的键名。 - - :param cache_name: 缓存名称 - :return: 缓存键名列表结果 - """ - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with self.redis_support.redis_session() as (redis, _redis_util): - cache_keys: list[str] = await redis.keys(self.redis_support.build_cache_name_keys_pattern(cache_name)) - key_items = self.domain_support.extract_cache_key_items(cache_name, cache_keys) - return {'ok': True, 'cacheName': cache_name, 'count': len(key_items), 'keys': key_items} - except redis_error as exc: - return self.redis_support.build_redis_error_result('读取缓存键名失败', exc) - - async def get_cache_value(self, cache_name: str, cache_key: str) -> dict[str, Any]: - """ - 读取指定缓存键值。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :return: 缓存值结果 - """ - redis_error = self.infrastructure_gateway.get_redis_error_class() - full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key) - try: - async with self.redis_support.redis_session() as (redis, _redis_util): - cache_value = await redis.get(full_cache_key) - if cache_value is None: - return self.domain_support.build_missing_cache_result(cache_name, cache_key) - return { - 'ok': True, - 'cacheName': cache_name, - 'cacheKey': cache_key, - 'fullCacheKey': full_cache_key, - 'cacheValue': cache_value, - } - except redis_error as exc: - return self.redis_support.build_redis_error_result('读取缓存内容失败', exc) - - async def get_cache_ttl(self, cache_name: str, cache_key: str) -> dict[str, Any]: - """ - 读取指定缓存键的剩余过期时间。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :return: 缓存 TTL 结果 - """ - redis_error = self.infrastructure_gateway.get_redis_error_class() - full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key) - try: - async with self.redis_support.redis_session() as (redis, _redis_util): - ttl_seconds = await redis.ttl(full_cache_key) - if ttl_seconds == REDIS_TTL_KEY_NOT_FOUND: - return self.domain_support.build_missing_cache_result(cache_name, cache_key) - return { - 'ok': True, - 'message': '缓存剩余过期时间读取成功' if ttl_seconds >= 0 else '缓存存在且未设置过期时间', - 'cacheName': cache_name, - 'cacheKey': cache_key, - 'fullCacheKey': full_cache_key, - 'ttlSeconds': ttl_seconds, - 'persistent': ttl_seconds == REDIS_TTL_PERSISTENT, - 'expires': ttl_seconds >= 0, - } - except redis_error as exc: - return self.redis_support.build_redis_error_result('读取缓存剩余过期时间失败', exc) - - async def clear_cache( - self, - *, - cache_name: str = '', - cache_key: str = '', - clear_all: bool = False, - dry_run: bool = False, - ) -> dict[str, Any]: - """ - 清理缓存。 - - :param cache_name: 需要按缓存名称清理的前缀 - :param cache_key: 需要按缓存键名模糊清理的键 - :param clear_all: 是否清理全部缓存 - :param dry_run: 是否仅演练执行 - :return: 缓存清理结果 - """ - selected_modes = [bool(cache_name), bool(cache_key), clear_all] - if sum(selected_modes) != 1: - return { - 'ok': False, - 'message': '必须且只能指定一种清理方式:--cache-name、--cache-key 或 --all', - 'exit_code': RUNTIME_ERROR, - } - - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with self.redis_support.redis_session() as (redis, redis_util): - target_pattern = self.redis_support.build_clear_target_pattern( - cache_name=cache_name, - cache_key=cache_key, - clear_all=clear_all, - ) - if target_pattern is None: - target_keys = sorted(await redis.keys()) - else: - target_keys = sorted(await redis.keys(target_pattern)) - - result = { - 'ok': True, - 'dryRun': dry_run, - 'matchedCount': len(target_keys), - 'matchedKeys': target_keys, - 'scope': self.domain_support.build_clear_scope( - cache_name=cache_name, - cache_key=cache_key, - clear_all=clear_all, - ), - } - if dry_run: - result['message'] = '缓存清理演练完成,未执行实际删除' - return result - - if target_keys: - await redis.delete(*target_keys) - - if clear_all: - await redis_util.init_sys_dict(redis) - await redis_util.init_sys_config(redis) - - result['message'] = '缓存清理完成' - return result - except redis_error as exc: - return self.redis_support.build_redis_error_result('清理缓存失败', exc) - - async def warmup_cache(self) -> dict[str, Any]: - """ - 预热系统缓存。 - - :return: 缓存预热执行结果 - """ - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with self.redis_support.redis_session() as (redis, redis_util): - await redis_util.init_sys_dict(redis) - await redis_util.init_sys_config(redis) - return {'ok': True, 'message': '缓存预热完成'} - except redis_error as exc: - return self.redis_support.build_redis_error_result('缓存预热失败', exc) - - -CACHE_RUNTIME = CacheRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/support.py deleted file mode 100644 index 72db4df..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/cache/support.py +++ /dev/null @@ -1,201 +0,0 @@ -from contextlib import asynccontextmanager -from typing import Any - -from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR - -from .gateway import CacheInfrastructureGateway - - -class CacheDomainSupport: - """ - 缓存领域支持对象。 - - 该对象负责缓存名称列表构建、键范围解析和 TTL 结果判定等 - 局部缓存规则,避免主运行时服务继续承载细碎领域判断。 - - :param infrastructure_gateway: 缓存基础设施网关 - """ - - def __init__(self, infrastructure_gateway: CacheInfrastructureGateway) -> None: - """ - 初始化缓存领域支持对象。 - - :param infrastructure_gateway: 缓存基础设施网关 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - - def build_cache_name_items(self) -> list[dict[str, Any]]: - """ - 构建系统内置缓存名称列表。 - - :return: 缓存名称列表 - """ - redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config() - return [ - { - 'cacheName': key_config.key, - 'remark': key_config.remark, - } - for key_config in redis_init_key_config - ] - - @staticmethod - def build_full_cache_key(cache_name: str, cache_key: str) -> str: - """ - 构建缓存完整键名。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :return: 完整缓存键名 - """ - return f'{cache_name}:{cache_key}' - - def build_missing_cache_result(self, cache_name: str, cache_key: str) -> dict[str, Any]: - """ - 构建缓存不存在的统一结果。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名 - :return: 缓存不存在结果 - """ - full_cache_key = self.build_full_cache_key(cache_name, cache_key) - return { - 'ok': False, - 'message': f'缓存不存在:{full_cache_key}', - 'cacheName': cache_name, - 'cacheKey': cache_key, - 'fullCacheKey': full_cache_key, - 'exit_code': RUNTIME_ERROR, - } - - @staticmethod - def build_cache_name_pattern(cache_name: str) -> str: - """ - 构建按缓存名称扫描时使用的 Redis 键模式。 - - :param cache_name: 缓存名称 - :return: Redis 键匹配模式 - """ - return f'{cache_name}:*' - - @staticmethod - def extract_cache_key_items(cache_name: str, cache_keys: list[str]) -> list[str]: - """ - 从缓存完整键名列表中提取相对键名。 - - :param cache_name: 缓存名称 - :param cache_keys: 完整缓存键名列表 - :return: 相对键名列表 - """ - return [key.split(':', 1)[1] for key in sorted(cache_keys) if key.startswith(f'{cache_name}:')] - - @staticmethod - def build_clear_scope( - *, - cache_name: str, - cache_key: str, - clear_all: bool, - ) -> dict[str, str]: - """ - 构建缓存清理范围定义。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名片段 - :param clear_all: 是否清理全部缓存 - :return: 清理范围定义 - """ - if clear_all: - return {'mode': 'all'} - if cache_name: - return {'mode': 'cacheName', 'cacheName': cache_name} - return {'mode': 'cacheKey', 'cacheKey': cache_key} - - -class CacheRedisSupport: - """ - 缓存 Redis 访问支持对象。 - - 该对象负责 Redis 连接生命周期、错误结果规整和缓存键模式拼装, - 避免主运行时服务继续承载基础设施桥接细节。 - - :param infrastructure_gateway: 缓存基础设施网关 - :param domain_support: 缓存领域支持对象 - """ - - def __init__( - self, - infrastructure_gateway: CacheInfrastructureGateway, - domain_support: CacheDomainSupport, - ) -> None: - """ - 初始化缓存 Redis 访问支持对象。 - - :param infrastructure_gateway: 缓存基础设施网关 - :param domain_support: 缓存领域支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - self.domain_support = domain_support - - @asynccontextmanager - async def redis_session(self) -> Any: - """ - 创建并托管 Redis 会话。 - - :return: `(redis, redis_util)` 元组 - """ - redis = None - redis_util = self.infrastructure_gateway.get_redis_util() - try: - redis = await redis_util.create_redis_pool(log_enabled=False) - yield redis, redis_util - finally: - if redis is not None: - await redis.close() - - @staticmethod - def build_redis_error_result(message: str, exc: Exception) -> dict[str, Any]: - """ - 构建统一 Redis 异常结果。 - - :param message: 失败消息 - :param exc: 原始异常 - :return: 标准失败结果 - """ - return { - 'ok': False, - 'message': message, - 'error': str(exc), - 'exit_code': REDIS_ERROR, - } - - def build_cache_name_keys_pattern(self, cache_name: str) -> str: - """ - 构建按缓存名称读取键列表时使用的模式。 - - :param cache_name: 缓存名称 - :return: Redis 键匹配模式 - """ - return self.domain_support.build_cache_name_pattern(cache_name) - - def build_clear_target_pattern( - self, - *, - cache_name: str, - cache_key: str, - clear_all: bool, - ) -> str | None: - """ - 构建缓存清理目标的 Redis 键匹配模式。 - - :param cache_name: 缓存名称 - :param cache_key: 缓存键名片段 - :param clear_all: 是否清理全部缓存 - :return: Redis 键匹配模式;全部清理时返回 `None` - """ - if clear_all: - return None - if cache_name: - return self.domain_support.build_cache_name_pattern(cache_name) - return f'*{cache_key}' diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/config/__init__.py deleted file mode 100644 index 3f60116..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import CONFIG_RUNTIME, ConfigRuntimeService - -__all__ = ['CONFIG_RUNTIME', 'ConfigRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/config/gateway.py deleted file mode 100644 index bd4eb6c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/gateway.py +++ /dev/null @@ -1,101 +0,0 @@ -from importlib import import_module -from typing import Any - - -class ConfigInfrastructureGateway: - """ - 参数配置基础设施网关。 - - 该对象负责延迟加载数据库会话、Redis、DAO、Service 与脱敏工具依赖, - 供参数配置运行时 facade 和其协作对象统一复用。 - """ - - @staticmethod - def get_redis_error_class() -> type[Exception]: - """ - 获取 Redis 异常类型。 - - :return: Redis 异常类型 - """ - return import_module('redis.exceptions').RedisError - - @staticmethod - def get_common_constant() -> Any: - """ - 获取公共常量定义。 - - :return: 公共常量定义模块 - """ - return import_module('common.constant').CommonConstant - - @staticmethod - def get_redis_init_key_config() -> Any: - """ - 获取 Redis 初始化键枚举。 - - :return: Redis 初始化键枚举 - """ - return import_module('common.enums').RedisInitKeyConfig - - @staticmethod - def get_page_model() -> Any: - """ - 获取分页模型类型。 - - :return: 分页模型类型 - """ - return import_module('common.vo').PageModel - - @staticmethod - def get_async_session_local() -> Any: - """ - 获取异步数据库会话工厂。 - - :return: 异步数据库会话工厂 - """ - return import_module('config.database').AsyncSessionLocal - - @staticmethod - def get_redis_util() -> Any: - """ - 获取 Redis 工具类。 - - :return: Redis 工具类 - """ - return import_module('config.get_redis').RedisUtil - - @staticmethod - def get_config_dao() -> Any: - """ - 获取参数配置 DAO。 - - :return: 参数配置 DAO - """ - return import_module('module_admin.dao.config_dao').ConfigDao - - @staticmethod - def get_config_vo_module() -> Any: - """ - 获取参数配置 VO 模块。 - - :return: 参数配置 VO 模块 - """ - return import_module('module_admin.entity.vo.config_vo') - - @staticmethod - def get_config_service() -> Any: - """ - 获取参数配置服务类。 - - :return: 参数配置服务类 - """ - return import_module('module_admin.service.config_service').ConfigService - - @staticmethod - def get_log_sanitizer() -> Any: - """ - 获取日志脱敏工具类。 - - :return: 日志脱敏工具类 - """ - return import_module('utils.log_util').LogSanitizer diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/config/service.py deleted file mode 100644 index af5331a..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/service.py +++ /dev/null @@ -1,406 +0,0 @@ -from typing import Any, Literal - -from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, REDIS_ERROR - -from .gateway import ConfigInfrastructureGateway -from .support import ConfigDomainSupport - - -class ConfigRuntimeService: - """ - 参数配置运行时服务。 - - 该服务作为参数配置运行时 facade,对外统一暴露参数配置列表、详情、 - 写入、缓存刷新与一致性诊断入口。 - - :param infrastructure_gateway: 参数配置基础设施网关 - :param domain_support: 参数配置领域支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: ConfigInfrastructureGateway | None = None, - domain_support: ConfigDomainSupport | None = None, - ) -> None: - """ - 初始化参数配置运行时服务。 - - :param infrastructure_gateway: 参数配置基础设施网关 - :param domain_support: 参数配置领域支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or ConfigInfrastructureGateway() - self.domain_support = domain_support or ConfigDomainSupport(self.infrastructure_gateway) - - @staticmethod - def build_cli_config_model(config_vo_module: Any, config_record: Any) -> Any | None: - """ - 将 ORM 配置记录显式转换为 CLI 可用的配置模型。 - - `ConfigModel` 当前未统一开启 `populate_by_name`,因此 CLI 侧在处理 - ORM 蛇形字段对象时,需要主动映射为驼峰 alias 输入,避免详情查询结果 - 被静默序列化为空。 - - :param config_vo_module: 配置 VO 模块 - :param config_record: ORM 配置记录 - :return: CLI 可用配置模型 - """ - if config_record is None: - return None - return config_vo_module.ConfigModel( - configId=getattr(config_record, 'config_id', None), - configName=getattr(config_record, 'config_name', None), - configKey=getattr(config_record, 'config_key', None), - configValue=getattr(config_record, 'config_value', None), - configType=getattr(config_record, 'config_type', None), - createBy=getattr(config_record, 'create_by', None), - createTime=getattr(config_record, 'create_time', None), - updateBy=getattr(config_record, 'update_by', None), - updateTime=getattr(config_record, 'update_time', None), - remark=getattr(config_record, 'remark', None), - ) - - async def load_config_from_database( - self, - config_key: str, - ) -> tuple[Any | None, dict[str, Any] | None] | dict[str, Any]: - """ - 从数据库加载参数配置。 - - :param config_key: 参数键名 - :return: 成功时返回配置模型与序列化结果,失败时返回错误结果字典 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - config_dao = self.infrastructure_gateway.get_config_dao() - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - try: - async with async_session_local() as session: - config_record = await config_dao.get_config_detail_by_info( - session, - config_vo_module.ConfigModel(configKey=config_key), - ) - except Exception as exc: - return {'ok': False, 'message': '读取参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - database_model = self.build_cli_config_model(config_vo_module, config_record) - return database_model, self.domain_support.serialize_config_model(database_model) - - async def load_config_from_cache( - self, - config_key: str, - ) -> tuple[str | None, dict[str, Any] | None] | dict[str, Any]: - """ - 从缓存加载参数配置。 - - :param config_key: 参数键名 - :return: 成功时返回原始缓存值与序列化结果,失败时返回错误结果字典 - """ - redis = None - redis_util = self.infrastructure_gateway.get_redis_util() - config_service = self.infrastructure_gateway.get_config_service() - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - redis = await redis_util.create_redis_pool(log_enabled=False) - cache_value = await config_service.query_config_list_from_cache_services(redis, config_key) - return cache_value, self.domain_support.serialize_cache_payload(config_key, cache_value) - except redis_error as exc: - return {'ok': False, 'message': '读取参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR} - finally: - if redis is not None: - await redis.close() - - async def list_configs( - self, - config_name: str = '', - config_key: str = '', - config_type: Literal['Y', 'N'] | None = None, - begin_date: str = '', - end_date: str = '', - *, - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 查询参数配置列表。 - - :param config_name: 参数名称过滤条件 - :param config_key: 参数键名过滤条件 - :param config_type: 参数类型过滤条件 - :param begin_date: 开始日期 - :param end_date: 结束日期 - :param paged: 是否开启分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: CLI 标准结果字典 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - config_service = self.infrastructure_gateway.get_config_service() - query_model = self.domain_support.build_config_query( - config_name, - config_key, - config_type, - begin_date, - end_date, - page_num, - page_size, - ) - try: - async with async_session_local() as session: - result = await config_service.get_config_list_services(session, query_model, is_page=paged) - except Exception as exc: - return {'ok': False, 'message': '读取参数配置列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - filters = self.domain_support.build_list_filters( - config_name=config_name, - config_key=config_key, - config_type=config_type, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - if paged and isinstance(result, self.infrastructure_gateway.get_page_model()): - page_payload = result.model_dump(by_alias=True) - page_payload['rows'] = [ - self.domain_support.sanitize_config_mapping(item) for item in page_payload.get('rows', []) - ] - return {'ok': True, 'filters': filters, 'page': page_payload} - - items = [self.domain_support.sanitize_config_mapping(item) for item in result] - return {'ok': True, 'filters': filters, 'count': len(items), 'items': items} - - async def get_config( - self, - config_key: str, - *, - source: Literal['db', 'cache', 'both'] = 'both', - ) -> dict[str, Any]: - """ - 按参数键读取配置详情。 - - :param config_key: 参数键名 - :param source: 读取来源 - :return: CLI 标准结果字典 - """ - database_model: Any | None = None - database_payload: dict[str, Any] | None = None - cache_value: str | None = None - cache_payload: dict[str, Any] | None = None - - if source in {'db', 'both'}: - database_result = await self.load_config_from_database(config_key) - if isinstance(database_result, dict): - return database_result - database_model, database_payload = database_result - - if source in {'cache', 'both'}: - cache_result = await self.load_config_from_cache(config_key) - if isinstance(cache_result, dict): - error_payload = {**cache_result, 'source': source} - if database_payload is not None: - error_payload['database'] = database_payload - return error_payload - cache_value, cache_payload = cache_result - - if source == 'db' and database_payload is None: - return self.domain_support.build_missing_config_result(config_key, source) - if source == 'cache' and cache_payload is None: - return self.domain_support.build_missing_config_result(config_key, source) - if source == 'both' and database_payload is None and cache_payload is None: - return self.domain_support.build_missing_config_result(config_key, source) - - payload = {'ok': True, 'key': config_key, 'source': source} - if source in {'db', 'both'}: - payload['database'] = database_payload - if source in {'cache', 'both'}: - payload['cache'] = cache_payload - if source == 'both': - payload['inSync'] = (database_model.config_value if database_model else None) == cache_value - return payload - - async def set_config( - self, - config_key: str, - config_value: str, - *, - config_name: str | None = None, - config_type: Literal['Y', 'N'] | None = None, - remark: str | None = None, - dry_run: bool = False, - ) -> dict[str, Any]: - """ - 新增或更新参数配置,并同步缓存。 - - :param config_key: 参数键名 - :param config_value: 参数键值 - :param config_name: 参数名称 - :param config_type: 参数类型 - :param remark: 备注 - :param dry_run: 是否仅执行演练 - :return: CLI 标准结果字典 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - config_dao = self.infrastructure_gateway.get_config_dao() - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - redis_util = self.infrastructure_gateway.get_redis_util() - redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config() - redis_error = self.infrastructure_gateway.get_redis_error_class() - existing_config = None - target_config = None - try: - async with async_session_local() as session: - config_record = await config_dao.get_config_detail_by_info( - session, - config_vo_module.ConfigModel(configKey=config_key), - ) - existing_config = self.build_cli_config_model(config_vo_module, config_record) - if existing_config is None and not config_name: - return { - 'ok': False, - 'message': '新增参数配置时必须传入 --name', - 'exit_code': ARGUMENT_ERROR, - } - - target_config = self.domain_support.build_target_config_model( - config_key, - config_value, - config_name, - config_type, - remark, - existing_config, - ) - if dry_run: - return { - 'ok': True, - 'message': '参数配置演练完成,未执行实际写入', - 'dryRun': True, - 'action': 'update' if existing_config else 'create', - 'config': self.domain_support.serialize_config_model(target_config), - } - - if existing_config: - await config_dao.edit_config_dao(session, target_config.model_dump(exclude_none=True)) - else: - await config_dao.add_config_dao(session, target_config) - await session.commit() - except Exception as exc: - return {'ok': False, 'message': '写入参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - redis = None - try: - redis = await redis_util.create_redis_pool(log_enabled=False) - await redis.set(f'{redis_init_key_config.SYS_CONFIG.key}:{config_key}', config_value) - except redis_error as exc: - return { - 'ok': False, - 'message': '参数配置已写入数据库,但同步缓存失败', - 'error': str(exc), - 'config': self.domain_support.serialize_config_model(target_config), - 'databaseCommitted': True, - 'exit_code': REDIS_ERROR, - } - finally: - if redis is not None: - await redis.close() - - return { - 'ok': True, - 'message': '参数配置已同步到数据库和缓存', - 'action': 'update' if existing_config else 'create', - 'config': self.domain_support.serialize_config_model(target_config), - } - - async def sync_config_cache(self) -> dict[str, Any]: - """ - 刷新参数配置缓存。 - - :return: CLI 标准结果字典 - """ - redis = None - async_session_local = self.infrastructure_gateway.get_async_session_local() - config_service = self.infrastructure_gateway.get_config_service() - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - redis_util = self.infrastructure_gateway.get_redis_util() - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with async_session_local() as session: - config_list = await config_service.get_config_list_services( - session, - config_vo_module.ConfigPageQueryModel(), - is_page=False, - ) - redis = await redis_util.create_redis_pool(log_enabled=False) - await config_service.init_cache_sys_config_services(session, redis) - except redis_error as exc: - return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR} - except Exception as exc: - return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - finally: - if redis is not None: - await redis.close() - - return {'ok': True, 'message': '参数缓存刷新成功', 'count': len(config_list)} - - async def diagnose_config(self, *, sample_limit: int = 10) -> dict[str, Any]: - """ - 诊断数据库参数配置与 Redis 缓存的一致性状态。 - - :param sample_limit: 示例键名输出上限 - :return: CLI 标准结果字典 - """ - redis = None - async_session_local = self.infrastructure_gateway.get_async_session_local() - config_service = self.infrastructure_gateway.get_config_service() - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - redis_util = self.infrastructure_gateway.get_redis_util() - redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config() - redis_error = self.infrastructure_gateway.get_redis_error_class() - try: - async with async_session_local() as session: - config_list = await config_service.get_config_list_services( - session, - config_vo_module.ConfigPageQueryModel(), - is_page=False, - ) - redis = await redis_util.create_redis_pool(log_enabled=False) - cache_prefix = f'{redis_init_key_config.SYS_CONFIG.key}:' - cache_keys = sorted(await redis.keys(f'{cache_prefix}*')) - cache_values = await redis.mget(*cache_keys) if cache_keys else [] - except redis_error as exc: - return {'ok': False, 'message': '读取参数缓存诊断信息失败', 'error': str(exc), 'exit_code': REDIS_ERROR} - except Exception as exc: - return {'ok': False, 'message': '读取参数配置诊断信息失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - finally: - if redis is not None: - await redis.close() - - database_map = {str(item.get('configKey')): str(item.get('configValue', '')) for item in config_list} - cache_map = { - cache_key.replace(cache_prefix, '', 1): '' if cache_value is None else str(cache_value) - for cache_key, cache_value in zip(cache_keys, cache_values, strict=False) - } - missing_in_cache = sorted(key for key in database_map if key not in cache_map) - orphan_in_cache = sorted(key for key in cache_map if key not in database_map) - mismatch_keys = sorted( - key for key, database_value in database_map.items() if key in cache_map and database_value != cache_map[key] - ) - in_sync = not missing_in_cache and not orphan_in_cache and not mismatch_keys - return { - 'ok': in_sync, - 'message': '参数配置数据库与缓存一致' if in_sync else '参数配置存在数据库与缓存不一致项', - 'databaseCount': len(database_map), - 'cacheCount': len(cache_map), - 'missingInCacheCount': len(missing_in_cache), - 'orphanInCacheCount': len(orphan_in_cache), - 'mismatchCount': len(mismatch_keys), - 'sampleLimit': sample_limit, - 'missingInCache': missing_in_cache[:sample_limit], - 'orphanInCache': orphan_in_cache[:sample_limit], - 'mismatchKeys': mismatch_keys[:sample_limit], - } - - -CONFIG_RUNTIME = ConfigRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/config/support.py deleted file mode 100644 index 268b1fd..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/config/support.py +++ /dev/null @@ -1,191 +0,0 @@ -from datetime import datetime -from typing import Any, Literal - -from cli.exit_codes import RUNTIME_ERROR -from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService - -from .gateway import ConfigInfrastructureGateway - - -class ConfigDomainSupport: - """ - 参数配置领域支持对象。 - - 该对象负责操作者解析、配置脱敏、序列化、查询模型构建与缺失结果构建, - 避免主运行时服务继续承载大量领域规则。 - - :param infrastructure_gateway: 参数配置基础设施网关 - """ - - def __init__( - self, - infrastructure_gateway: ConfigInfrastructureGateway, - operator_service: RuntimeOperatorService = RUNTIME_OPERATOR, - ) -> None: - """ - 初始化参数配置领域支持对象。 - - :param infrastructure_gateway: 参数配置基础设施网关 - :param operator_service: 运行时操作者解析服务 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - self.operator_service = operator_service - - def sanitize_config_mapping(self, config_mapping: dict[str, Any]) -> dict[str, Any]: - """ - 对配置字典进行脱敏处理。 - - :param config_mapping: 原始配置字典 - :return: 脱敏后的配置字典 - """ - return self.infrastructure_gateway.get_log_sanitizer().sanitize_data(config_mapping) - - def serialize_config_model(self, config_model: Any | None) -> dict[str, Any] | None: - """ - 将配置模型序列化为 CLI 输出字典。 - - :param config_model: 配置模型对象 - :return: 序列化后的配置字典 - """ - if config_model is None or config_model.config_id is None: - return None - return self.sanitize_config_mapping(config_model.model_dump(by_alias=True, exclude_none=True)) - - def serialize_cache_payload(self, config_key: str, config_value: str | None) -> dict[str, Any] | None: - """ - 将缓存中的配置值序列化为 CLI 输出字典。 - - :param config_key: 参数键名 - :param config_value: 参数键值 - :return: 序列化后的缓存配置字典 - """ - if config_value is None: - return None - return self.sanitize_config_mapping({'configKey': config_key, 'configValue': config_value}) - - @staticmethod - def build_missing_config_result( - config_key: str, - source: Literal['db', 'cache', 'both'], - ) -> dict[str, Any]: - """ - 构建配置不存在的统一结果。 - - :param config_key: 参数键名 - :param source: 读取来源 - :return: 配置不存在的结果字典 - """ - message = f'参数缓存不存在:{config_key}' if source == 'cache' else f'参数配置不存在:{config_key}' - return {'ok': False, 'message': message, 'source': source, 'exit_code': RUNTIME_ERROR} - - def build_config_query( - self, - config_name: str, - config_key: str, - config_type: Literal['Y', 'N'] | None, - begin_date: str, - end_date: str, - page_num: int, - page_size: int, - ) -> Any: - """ - 构建配置列表查询对象。 - - :param config_name: 参数名称过滤条件 - :param config_key: 参数键名过滤条件 - :param config_type: 参数类型过滤条件 - :param begin_date: 开始日期 - :param end_date: 结束日期 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 配置分页查询模型 - """ - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - return config_vo_module.ConfigPageQueryModel( - configName=config_name or None, - configKey=config_key or None, - configType=config_type, - beginTime=begin_date or None, - endTime=end_date or None, - pageNum=page_num, - pageSize=page_size, - ) - - def build_target_config_model( - self, - config_key: str, - config_value: str, - config_name: str | None, - config_type: Literal['Y', 'N'] | None, - remark: str | None, - existing_config: Any | None, - ) -> Any: - """ - 基于当前状态构建目标配置模型。 - - :param config_key: 参数键名 - :param config_value: 参数键值 - :param config_name: 参数名称 - :param config_type: 参数类型 - :param remark: 参数备注 - :param existing_config: 已存在的配置模型 - :return: 目标配置模型 - """ - operator = self.operator_service.resolve_operator() - current_time = datetime.now() - common_constant = self.infrastructure_gateway.get_common_constant() - config_vo_module = self.infrastructure_gateway.get_config_vo_module() - default_config_type = existing_config.config_type if existing_config else common_constant.NO - target_config = config_vo_module.ConfigModel( - configId=existing_config.config_id if existing_config else None, - configName=config_name - if config_name is not None - else (existing_config.config_name if existing_config else None), - configKey=config_key, - configValue=config_value, - configType=config_type if config_type is not None else default_config_type, - createBy=existing_config.create_by if existing_config else operator, - createTime=existing_config.create_time if existing_config else current_time, - updateBy=operator, - updateTime=current_time, - remark=remark if remark is not None else (existing_config.remark if existing_config else None), - ) - target_config.validate_fields() - return target_config - - def build_list_filters( - self, - *, - config_name: str, - config_key: str, - config_type: Literal['Y', 'N'] | None, - begin_date: str, - end_date: str, - paged: bool, - page_num: int, - page_size: int, - ) -> dict[str, Any]: - """ - 构建配置列表过滤条件。 - - :param config_name: 参数名称 - :param config_key: 参数键名 - :param config_type: 参数类型 - :param begin_date: 开始日期 - :param end_date: 结束日期 - :param paged: 是否分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 过滤条件字典 - """ - return { - 'configName': config_name, - 'configKey': config_key, - 'configType': config_type, - 'beginDate': begin_date, - 'endDate': end_date, - 'paged': paged, - 'pageNum': page_num, - 'pageSize': page_size, - } diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/__init__.py deleted file mode 100644 index 17dc068..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import CRYPTO_RUNTIME, CryptoRuntimeService - -__all__ = ['CRYPTO_RUNTIME', 'CryptoRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/gateway.py deleted file mode 100644 index 85b5ad5..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/gateway.py +++ /dev/null @@ -1,56 +0,0 @@ -from importlib import import_module -from typing import Any - - -class CryptoInfrastructureGateway: - """ - 传输加密基础设施网关。 - - 该对象负责延迟加载 cryptography、环境配置和传输加密工具依赖, - 供传输加密运行时 facade 与其协作对象统一复用。 - """ - - @staticmethod - def get_serialization_module() -> Any: - """ - 获取 cryptography 序列化模块。 - - :return: 序列化模块 - """ - return import_module('cryptography.hazmat.primitives.serialization') - - @staticmethod - def get_rsa_module() -> Any: - """ - 获取 RSA 非对称算法模块。 - - :return: RSA 模块 - """ - return import_module('cryptography.hazmat.primitives.asymmetric.rsa') - - @staticmethod - def get_transport_crypto_config() -> Any: - """ - 获取传输加密配置对象。 - - :return: 传输加密配置对象 - """ - return import_module('config.env').TransportCryptoConfig - - @staticmethod - def get_transport_crypto_util() -> Any: - """ - 获取传输加密工具类。 - - :return: 传输加密工具类 - """ - return import_module('utils.transport_crypto_util').TransportCryptoUtil - - @staticmethod - def get_transport_key_provider() -> Any: - """ - 获取传输加密密钥提供者。 - - :return: 传输加密密钥提供者 - """ - return import_module('utils.transport_crypto_util').TransportKeyProvider diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/service.py deleted file mode 100644 index 1e6ed69..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/service.py +++ /dev/null @@ -1,141 +0,0 @@ -from datetime import datetime -from typing import Any - -from .gateway import CryptoInfrastructureGateway -from .support import CryptoDomainSupport, CryptoResultSupport - - -class CryptoRuntimeService: - """ - 传输加密运行时服务。 - - 该服务作为传输加密运行时 facade,对外统一暴露配置校验、 - 公钥导出、密钥生成和轮换辅助方案构建入口。 - - :param infrastructure_gateway: 传输加密基础设施网关 - :param domain_support: 传输加密领域支持对象 - :param result_support: 传输加密结果支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: CryptoInfrastructureGateway | None = None, - domain_support: CryptoDomainSupport | None = None, - result_support: CryptoResultSupport | None = None, - ) -> None: - """ - 初始化传输加密运行时服务。 - - :param infrastructure_gateway: 传输加密基础设施网关 - :param domain_support: 传输加密领域支持对象 - :param result_support: 传输加密结果支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or CryptoInfrastructureGateway() - self.domain_support = domain_support or CryptoDomainSupport(self.infrastructure_gateway) - self.result_support = result_support or CryptoResultSupport() - - def generate_crypto_key_pair(self, kid: str, key_size: int) -> dict[str, Any]: - """ - 生成新的传输加密密钥对。 - - :param kid: 目标密钥版本标识 - :param key_size: RSA 密钥长度 - :return: 密钥生成结果 - """ - - def _operation() -> dict[str, Any]: - self.domain_support.validate_rsa_key_size(key_size) - private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size) - return { - 'ok': True, - 'message': '传输加密密钥生成完成', - 'kid': kid, - 'keySize': key_size, - 'privateKey': private_key_pem, - 'publicKey': public_key_pem, - 'envPatch': self.domain_support.build_key_pair_env_patch(kid, private_key_pem, public_key_pem), - } - - return self.result_support.run_argument_guarded(_operation, failure_message='生成传输加密密钥失败') - - def validate_crypto_config(self) -> dict[str, Any]: - """ - 校验传输加密运行配置。 - - :return: 传输加密配置校验结果 - """ - transport_key_provider = self.infrastructure_gateway.get_transport_key_provider() - - def _operation() -> dict[str, Any]: - transport_key_provider.validate_runtime_configuration() - return {'ok': True, 'message': '传输加密配置校验通过'} - - return self.result_support.run_runtime_guarded(_operation, failure_message='传输加密配置校验失败') - - def export_public_key(self) -> dict[str, Any]: - """ - 导出当前运行环境的传输加密公钥信息。 - - :return: 公钥导出结果 - """ - transport_key_provider = self.infrastructure_gateway.get_transport_key_provider() - transport_crypto_util = self.infrastructure_gateway.get_transport_crypto_util() - - def _operation() -> dict[str, Any]: - transport_key_provider.validate_runtime_configuration() - return {'ok': True, 'publicKey': transport_crypto_util.build_public_key_payload()} - - return self.result_support.run_runtime_guarded(_operation, failure_message='导出传输加密公钥失败') - - def build_rotation_payload(self, next_kid: str, key_size: int) -> dict[str, Any]: - """ - 生成传输加密密钥轮换辅助结果。 - - :param next_kid: 新密钥版本标识 - :param key_size: 新密钥的 RSA 长度 - :return: 轮换辅助结果 - """ - transport_key_provider = self.infrastructure_gateway.get_transport_key_provider() - - def _operation() -> dict[str, Any]: - self.domain_support.validate_rsa_key_size(key_size) - transport_key_provider.validate_runtime_configuration() - current_key_pair = transport_key_provider.get_current_key_pair() - if next_kid == current_key_pair.kid: - raise ValueError('新密钥版本不能与当前版本相同') - - private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size) - legacy_key_pairs = self.domain_support.load_existing_legacy_key_pairs() - merged_legacy_key_pairs = self.domain_support.merge_rotation_legacy_key_pairs( - legacy_key_pairs, - current_key_pair, - next_kid, - ) - return { - 'ok': True, - 'message': '传输加密密钥轮换方案已生成', - 'currentKid': current_key_pair.kid, - 'nextKid': next_kid, - 'keySize': key_size, - 'generatedAt': datetime.now().isoformat(), - 'nextKeyPair': { - 'kid': next_kid, - 'privateKey': private_key_pem, - 'publicKey': public_key_pem, - }, - 'legacyKeyPairs': merged_legacy_key_pairs, - 'envPatch': self.domain_support.build_rotation_env_patch( - next_kid, - private_key_pem, - public_key_pem, - merged_legacy_key_pairs, - ), - 'applySteps': self.result_support.build_rotation_apply_steps(), - } - - return self.result_support.run_argument_guarded(_operation, failure_message='生成密钥轮换方案失败') - - -CRYPTO_RUNTIME = CryptoRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/support.py deleted file mode 100644 index 73f2e81..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/crypto/support.py +++ /dev/null @@ -1,234 +0,0 @@ -import json -from collections.abc import Callable -from typing import Any - -from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR - -from .gateway import CryptoInfrastructureGateway - -MIN_RSA_KEY_SIZE = 2048 -RSA_KEY_SIZE_STEP = 256 - - -class CryptoDomainSupport: - """ - 传输加密领域支持对象。 - - 该对象负责 RSA 密钥规则校验、密钥对生成和历史密钥规整, - 避免主运行时服务继续承载局部安全规则和数据拼装细节。 - - :param infrastructure_gateway: 传输加密基础设施网关 - """ - - def __init__(self, infrastructure_gateway: CryptoInfrastructureGateway) -> None: - """ - 初始化传输加密领域支持对象。 - - :param infrastructure_gateway: 传输加密基础设施网关 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - - @staticmethod - def validate_rsa_key_size(key_size: int) -> None: - """ - 校验 RSA 密钥长度是否合法。 - - :param key_size: RSA 密钥长度 - :return: None - :raises ValueError: 密钥长度不满足约束时抛出异常 - """ - if key_size < MIN_RSA_KEY_SIZE or key_size % RSA_KEY_SIZE_STEP != 0: - raise ValueError('RSA 密钥长度必须大于等于 2048,且为 256 的整数倍') - - def generate_rsa_key_pair(self, key_size: int) -> tuple[str, str]: - """ - 生成 PEM 格式的 RSA 密钥对。 - - :param key_size: RSA 密钥长度 - :return: 私钥与公钥 PEM 字符串 - """ - rsa_module = self.infrastructure_gateway.get_rsa_module() - serialization_module = self.infrastructure_gateway.get_serialization_module() - private_key = rsa_module.generate_private_key(public_exponent=65537, key_size=key_size) - private_key_pem = private_key.private_bytes( - encoding=serialization_module.Encoding.PEM, - format=serialization_module.PrivateFormat.PKCS8, - encryption_algorithm=serialization_module.NoEncryption(), - ).decode('utf-8') - public_key_pem = ( - private_key.public_key() - .public_bytes( - encoding=serialization_module.Encoding.PEM, - format=serialization_module.PublicFormat.SubjectPublicKeyInfo, - ) - .decode('utf-8') - ) - return private_key_pem, public_key_pem - - def load_existing_legacy_key_pairs(self) -> list[dict[str, str]]: - """ - 解析当前配置中的历史密钥列表。 - - :return: 历史密钥列表 - :raises ValueError: 历史密钥配置非法时抛出异常 - """ - transport_crypto_config = self.infrastructure_gateway.get_transport_crypto_config() - raw_legacy_key_pairs = transport_crypto_config.transport_crypto_legacy_key_pairs or '[]' - parsed_legacy_key_pairs = json.loads(raw_legacy_key_pairs) - if not isinstance(parsed_legacy_key_pairs, list): - raise ValueError('TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS 必须为 JSON 数组') - return [item for item in parsed_legacy_key_pairs if isinstance(item, dict)] - - @staticmethod - def merge_rotation_legacy_key_pairs( - legacy_key_pairs: list[dict[str, str]], - current_key_pair: Any, - next_kid: str, - ) -> list[dict[str, str]]: - """ - 合并轮换后的历史密钥列表。 - - :param legacy_key_pairs: 当前历史密钥列表 - :param current_key_pair: 当前生效密钥对对象 - :param next_kid: 即将切换的新密钥版本 - :return: 合并后的历史密钥列表 - """ - normalized_legacy_key_pairs: dict[str, dict[str, str]] = { - str(item.get('kid')): item for item in legacy_key_pairs if item.get('kid') - } - normalized_legacy_key_pairs[current_key_pair.kid] = { - 'kid': current_key_pair.kid, - 'privateKey': current_key_pair.private_key_pem, - 'publicKey': current_key_pair.public_key_pem, - } - normalized_legacy_key_pairs.pop(next_kid, None) - return list(normalized_legacy_key_pairs.values()) - - @staticmethod - def build_key_pair_env_patch(kid: str, private_key_pem: str, public_key_pem: str) -> dict[str, str]: - """ - 构建密钥对写回环境变量所需的补丁数据。 - - :param kid: 密钥版本标识 - :param private_key_pem: 私钥 PEM 文本 - :param public_key_pem: 公钥 PEM 文本 - :return: 环境变量补丁数据 - """ - return { - 'TRANSPORT_CRYPTO_KID': kid, - 'TRANSPORT_CRYPTO_PRIVATE_KEY': private_key_pem, - 'TRANSPORT_CRYPTO_PUBLIC_KEY': public_key_pem, - } - - def build_rotation_env_patch( - self, - next_kid: str, - private_key_pem: str, - public_key_pem: str, - merged_legacy_key_pairs: list[dict[str, str]], - ) -> dict[str, str]: - """ - 构建密钥轮换写回环境变量所需的补丁数据。 - - :param next_kid: 新密钥版本标识 - :param private_key_pem: 新私钥 PEM 文本 - :param public_key_pem: 新公钥 PEM 文本 - :param merged_legacy_key_pairs: 合并后的历史密钥列表 - :return: 环境变量补丁数据 - """ - env_patch = self.build_key_pair_env_patch(next_kid, private_key_pem, public_key_pem) - env_patch['TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS'] = json.dumps(merged_legacy_key_pairs, ensure_ascii=False) - return env_patch - - -class CryptoResultSupport: - """ - 传输加密结果支持对象。 - - 该对象负责参数错误/运行时错误结果翻译,以及固定轮换提示文案构建, - 避免主运行时服务继续承载重复异常映射与静态 payload 片段。 - """ - - @staticmethod - def build_argument_error_result(message: str, exc: Exception) -> dict[str, Any]: - """ - 构建参数错误结果。 - - :param message: 失败消息 - :param exc: 原始异常 - :return: 标准参数错误结果 - """ - return { - 'ok': False, - 'message': message, - 'error': str(exc), - 'exit_code': ARGUMENT_ERROR, - } - - @staticmethod - def build_runtime_error_result(message: str, exc: Exception) -> dict[str, Any]: - """ - 构建运行时错误结果。 - - :param message: 失败消息 - :param exc: 原始异常 - :return: 标准运行时错误结果 - """ - return { - 'ok': False, - 'message': message, - 'error': str(exc), - 'exit_code': RUNTIME_ERROR, - } - - def run_argument_guarded( - self, - operation: Callable[[], dict[str, Any]], - *, - failure_message: str, - ) -> dict[str, Any]: - """ - 以统一参数/运行时错误翻译执行操作。 - - :param operation: 待执行操作 - :param failure_message: 失败消息 - :return: 操作结果 - """ - try: - return operation() - except ValueError as exc: - return self.build_argument_error_result(failure_message, exc) - except Exception as exc: - return self.build_runtime_error_result(failure_message, exc) - - def run_runtime_guarded( - self, - operation: Callable[[], dict[str, Any]], - *, - failure_message: str, - ) -> dict[str, Any]: - """ - 以统一运行时错误翻译执行操作。 - - :param operation: 待执行操作 - :param failure_message: 失败消息 - :return: 操作结果 - """ - try: - return operation() - except Exception as exc: - return self.build_runtime_error_result(failure_message, exc) - - @staticmethod - def build_rotation_apply_steps() -> list[str]: - """ - 构建密钥轮换后的应用步骤说明。 - - :return: 轮换应用步骤列表 - """ - return [ - '将 envPatch 中的新密钥写入目标环境配置', - '发布后保留 legacyKeyPairs,确保旧请求在轮换窗口内可解密', - '确认前端已刷新到 nextKid 后,再移除不再需要的历史密钥', - ] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/db/__init__.py deleted file mode 100644 index 2b1832e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import DATABASE_RUNTIME, DatabaseRuntimeService - -__all__ = ['DATABASE_RUNTIME', 'DatabaseRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/db/gateway.py deleted file mode 100644 index 20798e4..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/gateway.py +++ /dev/null @@ -1,56 +0,0 @@ -from importlib import import_module -from typing import Any - - -class DatabaseInfrastructureGateway: - """ - 数据库基础设施网关。 - - 该对象负责延迟加载数据库引擎、SQLAlchemy 和 Alembic 相关依赖, - 供数据库运行时 facade 与其协作对象统一复用。 - """ - - @staticmethod - def get_async_db_engine_factory() -> Any: - """ - 获取异步数据库引擎工厂函数。 - - :return: 异步数据库引擎工厂函数 - """ - return import_module('config.database').create_async_db_engine - - @staticmethod - def get_sync_db_engine_factory() -> Any: - """ - 获取同步数据库引擎工厂函数。 - - :return: 同步数据库引擎工厂函数 - """ - return import_module('config.database').create_sync_db_engine - - @staticmethod - def get_sqlalchemy_text() -> Any: - """ - 获取 SQLAlchemy `text` 构造函数。 - - :return: SQLAlchemy `text` 函数 - """ - return import_module('sqlalchemy').text - - @staticmethod - def get_alembic_config_class() -> Any: - """ - 获取 Alembic 配置类。 - - :return: Alembic 配置类 - """ - return import_module('alembic.config').Config - - @staticmethod - def get_alembic_script_directory_class() -> Any: - """ - 获取 Alembic 脚本目录类。 - - :return: Alembic 脚本目录类 - """ - return import_module('alembic.script').ScriptDirectory diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/db/service.py deleted file mode 100644 index f3ea221..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/service.py +++ /dev/null @@ -1,199 +0,0 @@ -from typing import Any - -from cli.exit_codes import DATABASE_ERROR -from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService - -from .gateway import DatabaseInfrastructureGateway -from .support import DatabaseAlembicCommandSupport, DatabaseRevisionSupport - - -class DatabaseRuntimeService: - """ - 数据库运行时服务。 - - 该服务作为数据库运行时 facade,对外统一暴露数据库连通性检查、 - Alembic 版本读取与迁移命令执行入口。 - - :param runtime_environment: 运行时环境服务 - :param infrastructure_gateway: 数据库基础设施网关 - :param revision_support: 数据库迁移版本支持对象 - :param alembic_command_support: 数据库 Alembic 命令支持对象 - """ - - def __init__( - self, - *, - runtime_environment: RuntimeEnvironmentService | None = None, - infrastructure_gateway: DatabaseInfrastructureGateway | None = None, - revision_support: DatabaseRevisionSupport | None = None, - alembic_command_support: DatabaseAlembicCommandSupport | None = None, - ) -> None: - """ - 初始化数据库运行时服务。 - - :param runtime_environment: 运行时环境服务 - :param infrastructure_gateway: 数据库基础设施网关 - :param revision_support: 数据库迁移版本支持对象 - :param alembic_command_support: 数据库 Alembic 命令支持对象 - :return: None - """ - self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT - self.infrastructure_gateway = infrastructure_gateway or DatabaseInfrastructureGateway() - self.revision_support = revision_support or DatabaseRevisionSupport( - self.infrastructure_gateway, - self.runtime_environment, - ) - self.alembic_command_support = alembic_command_support or DatabaseAlembicCommandSupport( - self.runtime_environment - ) - - async def ping_database(self) -> dict[str, Any]: - """ - 检查数据库连通性。 - - :return: 数据库检查结果 - """ - create_async_db_engine = self.infrastructure_gateway.get_async_db_engine_factory() - text = self.infrastructure_gateway.get_sqlalchemy_text() - engine = create_async_db_engine(echo=False) - try: - async with engine.connect() as connection: - await connection.execute(text('SELECT 1')) - return {'ok': True, 'message': '数据库连接成功'} - except Exception as exc: - return {'ok': False, 'message': '数据库连接失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - finally: - await engine.dispose() - - def get_current_revision(self) -> dict[str, Any]: - """ - 获取数据库当前迁移版本。 - - :return: 当前迁移版本信息 - """ - create_sync_db_engine = self.infrastructure_gateway.get_sync_db_engine_factory() - text = self.infrastructure_gateway.get_sqlalchemy_text() - engine = create_sync_db_engine(echo=False) - try: - with engine.connect() as connection: - revision = connection.execute(text('SELECT version_num FROM alembic_version')).scalar() - return {'ok': True, 'currentRevision': revision} - except Exception as exc: - return {'ok': False, 'message': '读取数据库迁移版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - finally: - engine.dispose() - - def upgrade_database(self, revision: str = 'head', *, dry_run: bool = False) -> dict[str, Any]: - """ - 执行数据库迁移升级。 - - :param revision: 目标迁移版本,默认为 `head` - :param dry_run: 是否仅演练执行 - :return: 数据库迁移执行结果 - """ - return self.alembic_command_support.run_alembic_command( - 'upgrade', - revision, - success_message=f'数据库已升级到 {revision}', - failure_message='数据库升级失败', - dry_run=dry_run, - ) - - def init_database(self, *, dry_run: bool = False) -> dict[str, Any]: - """ - 初始化数据库到最新迁移版本。 - - :param dry_run: 是否仅演练执行 - :return: 数据库初始化结果 - """ - return self.alembic_command_support.run_alembic_command( - 'upgrade', - 'head', - success_message='数据库初始化完成,当前版本已同步到 head', - failure_message='数据库初始化失败', - dry_run=dry_run, - ) - - def downgrade_database(self, revision: str = '-1', *, dry_run: bool = False) -> dict[str, Any]: - """ - 执行数据库回退。 - - :param revision: 目标回退版本,默认为 `-1` - :param dry_run: 是否仅演练执行 - :return: 数据库回退结果 - """ - return self.alembic_command_support.run_alembic_command( - 'downgrade', - revision, - success_message=f'数据库已回退到 {revision}', - failure_message='数据库回退失败', - dry_run=dry_run, - ) - - def create_revision(self, message: str, *, autogenerate: bool = False, dry_run: bool = False) -> dict[str, Any]: - """ - 创建新的 Alembic 迁移版本文件。 - - :param message: 迁移说明 - :param autogenerate: 是否自动生成变更 - :param dry_run: 是否仅演练执行 - :return: 迁移版本创建结果 - """ - arguments: list[str] = ['-m', message] - if autogenerate: - arguments.append('--autogenerate') - return self.alembic_command_support.run_alembic_command( - 'revision', - *arguments, - success_message='数据库迁移版本文件创建完成', - failure_message='数据库迁移版本文件创建失败', - dry_run=dry_run, - ) - - def get_alembic_heads(self) -> dict[str, Any]: - """ - 读取当前代码仓库中的 Alembic heads 信息。 - - :return: Alembic heads 结果 - """ - try: - script_directory = self.revision_support.build_alembic_script_directory() - items = [ - self.revision_support.serialize_revision(revision) - for revision in script_directory.get_revisions('heads') - ] - return { - 'ok': True, - 'message': '已读取 Alembic heads', - 'count': len(items), - 'items': items, - } - except Exception as exc: - return {'ok': False, 'message': '读取 Alembic heads 失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - def get_alembic_history(self, *, limit: int = 20) -> dict[str, Any]: - """ - 读取当前代码仓库中的 Alembic 历史版本信息。 - - :param limit: 返回的最大历史记录数量 - :return: Alembic 历史版本结果 - """ - try: - script_directory = self.revision_support.build_alembic_script_directory() - history_items = [ - self.revision_support.serialize_revision(revision) for revision in script_directory.walk_revisions() - ] - limited_items = history_items[:limit] - return { - 'ok': True, - 'message': '已读取 Alembic 历史版本', - 'count': len(limited_items), - 'totalCount': len(history_items), - 'limit': limit, - 'items': limited_items, - } - except Exception as exc: - return {'ok': False, 'message': '读取 Alembic 历史版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - -DATABASE_RUNTIME = DatabaseRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/db/support.py deleted file mode 100644 index 2f1cfcc..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/db/support.py +++ /dev/null @@ -1,160 +0,0 @@ -import os -import subprocess -from typing import Any - -from cli.exit_codes import DATABASE_ERROR -from cli.runtime.base import RuntimeEnvironmentService - -from .gateway import DatabaseInfrastructureGateway - - -class DatabaseRevisionSupport: - """ - 数据库迁移版本支持对象。 - - 该对象负责 Alembic script directory 构建、修订版本规整和 - 版本对象序列化,避免主运行时服务继续承载迁移元数据细节。 - - :param infrastructure_gateway: 数据库基础设施网关 - :param runtime_environment: 运行时环境服务 - """ - - def __init__( - self, - infrastructure_gateway: DatabaseInfrastructureGateway, - runtime_environment: RuntimeEnvironmentService, - ) -> None: - """ - 初始化数据库迁移版本支持对象。 - - :param infrastructure_gateway: 数据库基础设施网关 - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - self.runtime_environment = runtime_environment - - def build_alembic_script_directory(self) -> Any: - """ - 基于当前后端目录构建 Alembic 脚本目录对象。 - - :return: Alembic 脚本目录对象 - """ - config_class = self.infrastructure_gateway.get_alembic_config_class() - script_directory_class = self.infrastructure_gateway.get_alembic_script_directory_class() - config = config_class(os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini')) - return script_directory_class.from_config(config) - - @staticmethod - def normalize_revision_value(value: Any) -> list[str]: - """ - 规范化 Alembic 修订版本引用值。 - - :param value: 原始修订版本引用 - :return: 规范化后的修订版本列表 - """ - if value is None: - return [] - if isinstance(value, str): - return [value] - return [str(item) for item in value] - - def serialize_revision(self, script_revision: Any) -> dict[str, Any]: - """ - 序列化 Alembic 修订版本对象。 - - :param script_revision: Alembic 修订版本对象 - :return: 序列化后的修订版本字典 - """ - return { - 'revision': str(script_revision.revision), - 'downRevisions': self.normalize_revision_value(script_revision.down_revision), - 'branchLabels': sorted(str(item) for item in script_revision.branch_labels or []), - 'dependsOn': self.normalize_revision_value(script_revision.dependencies), - 'doc': (script_revision.doc or '').strip(), - 'path': str(script_revision.path), - } - - -class DatabaseAlembicCommandSupport: - """ - 数据库 Alembic 命令支持对象。 - - 该对象负责 Alembic 命令组装与子进程执行, - 避免主运行时服务继续承载命令行桥接细节。 - - :param runtime_environment: 运行时环境服务 - """ - - def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None: - """ - 初始化数据库 Alembic 命令支持对象。 - - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.runtime_environment = runtime_environment - - def build_alembic_command(self, command: str, *arguments: str) -> list[str]: - """ - 构建 Alembic 命令。 - - :param command: Alembic 子命令名称 - :param arguments: Alembic 子命令参数列表 - :return: Alembic 命令参数列表 - """ - alembic_ini_path = os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini') - return ['alembic', '-c', alembic_ini_path, command, *arguments] - - def run_alembic_command( - self, - command: str, - *arguments: str, - success_message: str, - failure_message: str, - dry_run: bool = False, - ) -> dict[str, Any]: - """ - 执行 Alembic 命令并返回统一结果。 - - :param command: Alembic 子命令名称 - :param arguments: Alembic 子命令参数列表 - :param success_message: 成功提示信息 - :param failure_message: 失败提示信息 - :param dry_run: 是否仅演练执行 - :return: Alembic 执行结果 - """ - command_arguments = self.build_alembic_command(command, *arguments) - if dry_run: - return { - 'ok': True, - 'message': f'{success_message}(dry-run)', - 'dryRun': True, - 'command': command_arguments, - 'cwd': self.runtime_environment.get_backend_dir(), - } - - try: - completed = subprocess.run( - command_arguments, - cwd=self.runtime_environment.get_backend_dir(), - env={**os.environ, 'APP_ENV': os.environ.get('APP_ENV', 'dev')}, - capture_output=True, - text=True, - check=False, - ) - if completed.returncode != 0: - return { - 'ok': False, - 'message': failure_message, - 'error': completed.stderr.strip() or completed.stdout.strip(), - 'exit_code': DATABASE_ERROR, - } - - payload: dict[str, Any] = {'ok': True, 'message': success_message} - stdout = completed.stdout.strip() - if stdout: - payload['stdout'] = stdout - return payload - except Exception as exc: - return {'ok': False, 'message': failure_message, 'error': str(exc), 'exit_code': DATABASE_ERROR} diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/__init__.py deleted file mode 100644 index e5316cd..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService - -__all__ = ['DEVELOPMENT_RUNTIME', 'DevelopmentRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/gateway.py deleted file mode 100644 index 7ea2c11..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/gateway.py +++ /dev/null @@ -1,60 +0,0 @@ -import subprocess -from typing import Any - -from cli.exit_codes import RUNTIME_ERROR -from cli.runtime.base import RuntimeEnvironmentService - - -class DevelopmentProcessGateway: - """ - 开发子进程执行网关。 - - 该对象负责统一执行开发工具子进程命令, - 避免支持对象直接依赖 `subprocess` 细节。 - - :param runtime_environment: 运行时环境服务 - """ - - def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None: - """ - 初始化开发子进程执行网关。 - - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.runtime_environment = runtime_environment - - def run_command(self, command: list[str]) -> dict[str, Any]: - """ - 执行子进程命令并返回统一结果。 - - :param command: 待执行命令 - :return: 命令执行结果 - """ - try: - completed = subprocess.run( - command, - cwd=self.runtime_environment.get_backend_dir(), - capture_output=True, - text=True, - check=False, - ) - except Exception as exc: - return { - 'ok': False, - 'message': '开发命令执行失败', - 'error': str(exc), - 'command': command, - 'exit_code': RUNTIME_ERROR, - } - - payload: dict[str, Any] = { - 'ok': completed.returncode == 0, - 'command': command, - 'returnCode': completed.returncode, - } - if completed.stdout.strip(): - payload['stdout'] = completed.stdout.strip() - if completed.stderr.strip(): - payload['stderr'] = completed.stderr.strip() - return payload diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/service.py deleted file mode 100644 index 5812c75..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/service.py +++ /dev/null @@ -1,158 +0,0 @@ -from typing import Any - -from cli.exit_codes import DEPENDENCY_ERROR, RUNTIME_ERROR -from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService - -from .gateway import DevelopmentProcessGateway -from .support import DevelopmentCommandSupport, DevelopmentToolingSupport - - -class DevelopmentRuntimeService: - """ - 开发运行时服务。 - - 该服务作为开发运行时 facade,对外统一暴露 lint/format 和 pytest - 执行入口。 - - :param runtime_environment: 运行时环境服务 - :param tooling_support: 开发工具支持对象 - :param command_support: 开发命令构建支持对象 - :param process_gateway: 开发子进程执行网关 - """ - - def __init__( - self, - *, - runtime_environment: RuntimeEnvironmentService | None = None, - tooling_support: DevelopmentToolingSupport | None = None, - command_support: DevelopmentCommandSupport | None = None, - process_gateway: DevelopmentProcessGateway | None = None, - ) -> None: - """ - 初始化开发运行时服务。 - - :param runtime_environment: 运行时环境服务 - :param tooling_support: 开发工具支持对象 - :param command_support: 开发命令构建支持对象 - :param process_gateway: 开发子进程执行网关 - :return: None - """ - self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT - self.tooling_support = tooling_support or DevelopmentToolingSupport() - self.command_support = command_support or DevelopmentCommandSupport(self.runtime_environment) - self.process_gateway = process_gateway or DevelopmentProcessGateway(self.runtime_environment) - - def run_lint( - self, - targets: list[str] | None, - *, - check_only: bool = False, - fix: bool = False, - unsafe_fixes: bool = False, - ) -> dict[str, Any]: - """ - 执行仓库 lint/format 检查。 - - :param targets: 目标路径列表 - :param check_only: 是否仅检查不写回 - :param fix: 是否允许 `ruff check --fix` - :param unsafe_fixes: 是否允许不安全修复 - :return: lint 执行结果 - """ - normalized_targets = self.tooling_support.resolve_targets(targets) - format_command = self.command_support.build_format_command(normalized_targets, check_only=check_only) - format_result = self.process_gateway.run_command(format_command) - if not format_result.get('ok', False): - return { - 'ok': False, - 'message': 'Ruff format 阶段失败', - 'targets': normalized_targets, - 'format': format_result, - 'exit_code': RUNTIME_ERROR, - } - - check_command = self.command_support.build_check_command( - normalized_targets, - check_only=check_only, - fix=fix, - unsafe_fixes=unsafe_fixes, - ) - check_result = self.process_gateway.run_command(check_command) - if not check_result.get('ok', False): - return { - 'ok': False, - 'message': 'Ruff check 阶段失败', - 'targets': normalized_targets, - 'format': format_result, - 'check': check_result, - 'exit_code': RUNTIME_ERROR, - } - - return { - 'ok': True, - 'message': '开发检查已完成', - 'targets': normalized_targets, - 'checkOnly': check_only, - 'fix': fix and not check_only, - 'unsafeFixes': unsafe_fixes and fix and not check_only, - 'format': format_result, - 'check': check_result, - } - - def run_tests( - self, - targets: list[str] | None, - *, - keyword: str = '', - maxfail: int = 0, - quiet: bool = False, - ) -> dict[str, Any]: - """ - 执行项目测试。 - - :param targets: 目标测试路径列表 - :param keyword: pytest `-k` 过滤表达式 - :param maxfail: 最大失败数,0 表示不限制 - :param quiet: 是否启用简洁输出 - :return: 测试执行结果 - """ - if not self.tooling_support.is_pytest_available(): - return { - 'ok': False, - 'message': 'pytest 未安装,无法执行 dev test', - 'hint': '请在当前环境安装 pytest 后重试', - 'exit_code': DEPENDENCY_ERROR, - } - - normalized_targets = self.tooling_support.resolve_targets(targets) - command = self.command_support.build_pytest_command( - normalized_targets, - keyword=keyword, - maxfail=maxfail, - quiet=quiet, - ) - test_result = self.process_gateway.run_command(command) - if not test_result.get('ok', False): - return { - 'ok': False, - 'message': '测试执行失败', - 'targets': normalized_targets, - 'keyword': keyword, - 'maxfail': maxfail, - 'quiet': quiet, - 'test': test_result, - 'exit_code': RUNTIME_ERROR, - } - - return { - 'ok': True, - 'message': '测试执行完成', - 'targets': normalized_targets, - 'keyword': keyword, - 'maxfail': maxfail, - 'quiet': quiet, - 'test': test_result, - } - - -DEVELOPMENT_RUNTIME = DevelopmentRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/support.py deleted file mode 100644 index 4f37d2b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/dev/support.py +++ /dev/null @@ -1,118 +0,0 @@ -import importlib.util - -from cli.runtime.base import RuntimeEnvironmentService - - -class DevelopmentToolingSupport: - """ - 开发工具支持对象。 - - 该对象负责目标列表规整与开发依赖可用性检查, - 避免主运行时服务继续承载轻量工具规则。 - """ - - @staticmethod - def resolve_targets(targets: list[str] | None) -> list[str]: - """ - 规范化命令目标列表。 - - :param targets: 原始目标列表 - :return: 规范化后的目标列表 - """ - normalized_targets = [target.strip() for target in (targets or []) if target.strip()] - return normalized_targets or ['.'] - - @staticmethod - def is_pytest_available() -> bool: - """ - 检查 pytest 是否已安装。 - - :return: pytest 是否可用 - """ - return importlib.util.find_spec('pytest') is not None - - -class DevelopmentCommandSupport: - """ - 开发命令构建支持对象。 - - 该对象负责开发工具命令参数拼装, - 避免主运行时服务继续承载命令行桥接细节。 - - :param runtime_environment: 运行时环境服务 - """ - - def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None: - """ - 初始化开发命令构建支持对象。 - - :param runtime_environment: 运行时环境服务 - :return: None - """ - self.runtime_environment = runtime_environment - - def build_format_command(self, normalized_targets: list[str], *, check_only: bool) -> list[str]: - """ - 构建 Ruff format 命令。 - - :param normalized_targets: 已规整目标列表 - :param check_only: 是否仅检查 - :return: 格式化命令参数列表 - """ - command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'format'] - if check_only: - command.append('--check') - command.extend(normalized_targets) - return command - - def build_check_command( - self, - normalized_targets: list[str], - *, - check_only: bool, - fix: bool, - unsafe_fixes: bool, - ) -> list[str]: - """ - 构建 Ruff check 命令。 - - :param normalized_targets: 已规整目标列表 - :param check_only: 是否仅检查 - :param fix: 是否允许自动修复 - :param unsafe_fixes: 是否允许不安全修复 - :return: 检查命令参数列表 - """ - command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'check'] - if fix and not check_only: - command.append('--fix') - if unsafe_fixes: - command.append('--unsafe-fixes') - command.extend(normalized_targets) - return command - - def build_pytest_command( - self, - normalized_targets: list[str], - *, - keyword: str, - maxfail: int, - quiet: bool, - ) -> list[str]: - """ - 构建 pytest 命令。 - - :param normalized_targets: 已规整目标列表 - :param keyword: pytest `-k` 表达式 - :param maxfail: 最大失败数 - :param quiet: 是否启用简洁输出 - :return: pytest 命令参数列表 - """ - command = [self.runtime_environment.get_python_executable(), '-m', 'pytest'] - if quiet: - command.append('-q') - if keyword: - command.extend(['-k', keyword]) - if maxfail > 0: - command.append(f'--maxfail={maxfail}') - command.extend(normalized_targets) - return command diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/__init__.py deleted file mode 100644 index 0f23c5c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import GEN_RUNTIME, GenRuntimeService - -__all__ = ['GEN_RUNTIME', 'GenRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/gateway.py deleted file mode 100644 index e099a6a..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/gateway.py +++ /dev/null @@ -1,110 +0,0 @@ -from importlib import import_module -from typing import Any - - -class GenInfrastructureGateway: - """ - 代码生成基础设施网关。 - - 该对象负责延迟加载 sqlglot、数据库会话、分页模型、配置对象、 - 业务异常和代码生成服务依赖,供代码生成运行时 facade 与其协作对象统一复用。 - """ - - @staticmethod - def get_sqlglot_module() -> Any: - """ - 获取 `sqlglot` 模块。 - - :return: `sqlglot` 模块 - """ - return import_module('sqlglot') - - @staticmethod - def get_sqlglot_expressions_module() -> Any: - """ - 获取 `sqlglot.expressions` 模块。 - - :return: `sqlglot.expressions` 模块 - """ - return import_module('sqlglot.expressions') - - @staticmethod - def get_async_session_local() -> Any: - """ - 获取异步数据库会话工厂。 - - :return: 异步数据库会话工厂 - """ - return import_module('config.database').AsyncSessionLocal - - @staticmethod - def get_page_model() -> Any: - """ - 获取分页模型类型。 - - :return: 分页模型类型 - """ - return import_module('common.vo').PageModel - - @staticmethod - def get_database_config() -> Any: - """ - 获取数据库配置对象。 - - :return: 数据库配置对象 - """ - return import_module('config.env').DataBaseConfig - - @staticmethod - def get_gen_config() -> Any: - """ - 获取代码生成配置对象。 - - :return: 代码生成配置对象 - """ - return import_module('config.env').GenConfig - - @staticmethod - def get_service_exception_class() -> type[Exception]: - """ - 获取业务异常类型。 - - :return: 业务异常类型 - """ - return import_module('exceptions.exception').ServiceException - - @staticmethod - def get_user_vo_module() -> Any: - """ - 获取用户 VO 模块。 - - :return: 用户 VO 模块 - """ - return import_module('module_admin.entity.vo.user_vo') - - @staticmethod - def get_gen_vo_module() -> Any: - """ - 获取代码生成 VO 模块。 - - :return: 代码生成 VO 模块 - """ - return import_module('module_generator.entity.vo.gen_vo') - - @staticmethod - def get_gen_table_service() -> Any: - """ - 获取代码生成服务类。 - - :return: 代码生成服务类 - """ - return import_module('module_generator.service.gen_service').GenTableService - - @staticmethod - def get_gen_table_column_service() -> Any: - """ - 获取代码生成字段服务类。 - - :return: 代码生成字段服务类 - """ - return import_module('module_generator.service.gen_service').GenTableColumnService diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/service.py deleted file mode 100644 index e760da9..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/service.py +++ /dev/null @@ -1,412 +0,0 @@ -from typing import Any - -from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, RUNTIME_ERROR - -from .gateway import GenInfrastructureGateway -from .support import GenDomainSupport - - -class GenRuntimeService: - """ - 代码生成运行时服务。 - - 该服务作为代码生成运行时 facade,对外统一暴露业务表、数据库物理表、 - 建表 SQL、代码预览、导出与数据库同步等入口。 - - :param infrastructure_gateway: 代码生成基础设施网关 - :param domain_support: 代码生成领域支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: GenInfrastructureGateway | None = None, - domain_support: GenDomainSupport | None = None, - ) -> None: - """ - 初始化代码生成运行时服务。 - - :param infrastructure_gateway: 代码生成基础设施网关 - :param domain_support: 代码生成领域支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or GenInfrastructureGateway() - self.domain_support = domain_support or GenDomainSupport(self.infrastructure_gateway) - - async def import_tables(self, table_names: list[str], *, dry_run: bool = False) -> dict[str, Any]: - """ - 导入数据库表到代码生成业务表。 - - :param table_names: 待导入表名列表 - :param dry_run: 是否仅演练执行 - :return: 导入结果 - """ - normalized_table_names = self.domain_support.normalize_table_names(table_names) - if not normalized_table_names: - return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR} - - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - service_exception = self.infrastructure_gateway.get_service_exception_class() - try: - async with async_session_local() as session: - gen_table_list = await gen_table_service.get_gen_db_table_list_by_name_services( - session, - normalized_table_names, - ) - matched_table_names = [gen_table.table_name for gen_table in gen_table_list if gen_table.table_name] - missing_table_names = [ - table_name for table_name in normalized_table_names if table_name not in matched_table_names - ] - - if dry_run: - return { - 'ok': True, - 'message': '导入表结构演练完成,未执行实际写入', - 'dryRun': True, - 'requestedTables': normalized_table_names, - 'matchedTables': matched_table_names, - 'missingTables': missing_table_names, - } - - result = await gen_table_service.import_gen_table_services( - session, - gen_table_list, - self.domain_support.build_cli_current_user(), - ) - except service_exception as exc: - return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - except Exception as exc: - return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - return { - 'ok': bool(result.is_success), - 'message': result.message, - 'requestedTables': normalized_table_names, - } - - async def list_gen_tables( - self, - *, - table_name: str = '', - table_comment: str = '', - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 查询代码生成业务表列表。 - - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 查询结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - try: - gen_vo_module = self.infrastructure_gateway.get_gen_vo_module() - query_model = gen_vo_module.GenTablePageQueryModel( - tableName=table_name or None, - tableComment=table_comment or None, - pageNum=page_num, - pageSize=page_size, - ) - async with async_session_local() as session: - result = await gen_table_service.get_gen_table_list_services(session, query_model, is_page=paged) - except Exception as exc: - return { - 'ok': False, - 'message': '读取代码生成业务表列表失败', - 'error': str(exc), - 'exit_code': DATABASE_ERROR, - } - - filters = { - 'tableName': table_name, - 'tableComment': table_comment, - 'paged': paged, - 'pageNum': page_num, - 'pageSize': page_size, - } - return self.domain_support.build_list_payload(result, filters=filters, paged=paged) - - async def list_gen_db_tables( - self, - *, - table_name: str = '', - table_comment: str = '', - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 查询数据库中可导入的物理表列表。 - - :param table_name: 表名称过滤条件 - :param table_comment: 表描述过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 查询结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - try: - gen_vo_module = self.infrastructure_gateway.get_gen_vo_module() - query_model = gen_vo_module.GenTablePageQueryModel( - tableName=table_name or None, - tableComment=table_comment or None, - pageNum=page_num, - pageSize=page_size, - ) - async with async_session_local() as session: - result = await gen_table_service.get_gen_db_table_list_services(session, query_model, is_page=paged) - except Exception as exc: - return {'ok': False, 'message': '读取数据库表列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - filters = { - 'tableName': table_name, - 'tableComment': table_comment, - 'paged': paged, - 'pageNum': page_num, - 'pageSize': page_size, - } - return self.domain_support.build_list_payload(result, filters=filters, paged=paged) - - async def create_tables(self, sql: str, sql_file: str, *, dry_run: bool = False) -> dict[str, Any]: - """ - 根据 SQL 创建表并导入代码生成业务表。 - - :param sql: 直接传入的 SQL 文本 - :param sql_file: SQL 文件路径 - :param dry_run: 是否仅演练执行 - :return: 创建结果 - """ - try: - sql_text = self.domain_support.resolve_sql_text(sql, sql_file) - sql_statements, table_names = self.domain_support.parse_create_table_sql(sql_text) - except ValueError as exc: - return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': ARGUMENT_ERROR} - except Exception as exc: - return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR} - - if dry_run: - return { - 'ok': True, - 'message': '建表语句演练完成,未执行实际建表', - 'dryRun': True, - 'statementCount': len(sql_statements), - 'tableNames': table_names, - 'sql': sql_text, - } - - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - service_exception = self.infrastructure_gateway.get_service_exception_class() - try: - async with async_session_local() as session: - result = await gen_table_service.create_table_services( - session, - sql_text, - self.domain_support.build_cli_current_user(), - ) - except service_exception as exc: - return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - except Exception as exc: - return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - return { - 'ok': bool(result.is_success), - 'message': result.message, - 'tableNames': table_names, - } - - async def preview_code(self, table_id: int) -> dict[str, Any]: - """ - 预览指定业务表的代码生成结果。 - - :param table_id: 业务表 ID - :return: 预览结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - service_exception = self.infrastructure_gateway.get_service_exception_class() - try: - async with async_session_local() as session: - preview_payload = await gen_table_service.preview_code_services(session, table_id) - except service_exception as exc: - return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - except Exception as exc: - return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - return { - 'ok': True, - 'tableId': table_id, - 'templateCount': len(preview_payload), - 'preview': preview_payload, - } - - async def get_gen_table_detail(self, table_id: int) -> dict[str, Any]: - """ - 读取单个代码生成业务表详情。 - - :param table_id: 业务表 ID - :return: 详情结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - gen_table_column_service = self.infrastructure_gateway.get_gen_table_column_service() - try: - async with async_session_local() as session: - info = await gen_table_service.get_gen_table_by_id_services(session, table_id) - if not getattr(info, 'table_id', None): - return { - 'ok': False, - 'message': f'代码生成业务表不存在:{table_id}', - 'tableId': table_id, - 'exit_code': RUNTIME_ERROR, - } - rows = await gen_table_column_service.get_gen_table_column_list_by_table_id_services(session, table_id) - tables = await gen_table_service.get_gen_table_all_services(session) - except Exception as exc: - return { - 'ok': False, - 'message': '读取代码生成业务表详情失败', - 'error': str(exc), - 'exit_code': DATABASE_ERROR, - } - - detail_payload = { - 'info': self.domain_support.serialize_gen_item(info), - 'rows': self.domain_support.serialize_gen_items(rows), - 'tables': self.domain_support.serialize_gen_items(tables), - } - return { - 'ok': True, - 'tableId': table_id, - 'tableName': detail_payload['info'].get('tableName', ''), - 'columnCount': len(detail_payload['rows']), - 'tableCount': len(detail_payload['tables']), - 'detail': detail_payload, - } - - async def export_code( - self, - table_names: list[str], - *, - mode: str = 'zip', - output_file: str = '', - dry_run: bool = False, - ) -> dict[str, Any]: - """ - 导出代码生成结果。 - - :param table_names: 业务表名称列表 - :param mode: 导出模式,支持 `zip` 与 `local` - :param output_file: zip 导出目标文件路径 - :param dry_run: 是否仅演练执行 - :return: 导出结果 - """ - normalized_table_names = self.domain_support.normalize_table_names(table_names) - if not normalized_table_names: - return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR} - if mode not in {'zip', 'local'}: - return {'ok': False, 'message': '导出模式仅支持 zip 或 local', 'exit_code': ARGUMENT_ERROR} - - gen_config = self.infrastructure_gateway.get_gen_config() - if mode == 'local' and not gen_config.allow_overwrite: - return { - 'ok': False, - 'message': '当前系统配置不允许生成文件覆盖到本地', - 'hint': '请检查 GenConfig.allow_overwrite 配置', - 'exit_code': RUNTIME_ERROR, - } - - if dry_run: - dry_run_payload: dict[str, Any] = { - 'ok': True, - 'message': '代码导出演练完成,未执行实际导出', - 'dryRun': True, - 'mode': mode, - 'tableNames': normalized_table_names, - } - if mode == 'zip': - target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip' - dry_run_payload['outputFile'] = self.domain_support.resolve_output_file_path(target_file) - else: - dry_run_payload['genPath'] = gen_config.GEN_PATH - return dry_run_payload - - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - service_exception = self.infrastructure_gateway.get_service_exception_class() - try: - async with async_session_local() as session: - if mode == 'zip': - zip_bytes = await gen_table_service.batch_gen_code_services(session, normalized_table_names) - else: - messages = [] - for table_name in normalized_table_names: - result = await gen_table_service.generate_code_services(session, table_name) - messages.append({'tableName': table_name, 'message': result.message, 'ok': result.is_success}) - return { - 'ok': True, - 'message': '代码已生成到本地目录', - 'mode': mode, - 'tableNames': normalized_table_names, - 'genPath': gen_config.GEN_PATH, - 'results': messages, - } - except service_exception as exc: - return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - except Exception as exc: - return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip' - try: - target_path = self.domain_support.write_export_zip(target_file, zip_bytes) - except Exception as exc: - return {'ok': False, 'message': '写出导出文件失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR} - - return { - 'ok': True, - 'message': '代码压缩包导出完成', - 'mode': mode, - 'tableNames': normalized_table_names, - 'outputFile': target_path, - 'size': len(zip_bytes), - } - - async def sync_gen_table_from_db(self, table_name: str) -> dict[str, Any]: - """ - 将指定代码生成业务表与数据库表结构进行同步。 - - :param table_name: 业务表名称 - :return: 同步结果 - """ - normalized_table_name = table_name.strip() - if not normalized_table_name: - return {'ok': False, 'message': '表名不能为空', 'exit_code': ARGUMENT_ERROR} - - async_session_local = self.infrastructure_gateway.get_async_session_local() - gen_table_service = self.infrastructure_gateway.get_gen_table_service() - service_exception = self.infrastructure_gateway.get_service_exception_class() - try: - async with async_session_local() as session: - result = await gen_table_service.sync_db_services(session, normalized_table_name) - except service_exception as exc: - return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - except Exception as exc: - return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - return { - 'ok': bool(result.is_success), - 'message': result.message, - 'tableName': normalized_table_name, - } - - -GEN_RUNTIME = GenRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/support.py deleted file mode 100644 index 6f92be7..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/gen/support.py +++ /dev/null @@ -1,191 +0,0 @@ -from pathlib import Path -from typing import Any - -from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService - -from .gateway import GenInfrastructureGateway - - -class GenDomainSupport: - """ - 代码生成领域支持对象。 - - 该对象负责 CLI 当前用户构建、表名规整、记录序列化、SQL 解析 - 以及导出路径处理,避免主运行时服务继续承载过多局部规则。 - - :param infrastructure_gateway: 代码生成基础设施网关 - """ - - def __init__( - self, - infrastructure_gateway: GenInfrastructureGateway, - operator_service: RuntimeOperatorService = RUNTIME_OPERATOR, - ) -> None: - """ - 初始化代码生成领域支持对象。 - - :param infrastructure_gateway: 代码生成基础设施网关 - :param operator_service: 运行时操作者解析服务 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - self.operator_service = operator_service - - def build_cli_current_user(self) -> Any: - """ - 构建 CLI 场景使用的最小当前用户模型。 - - :return: CLI 当前用户模型 - """ - operator = self.operator_service.resolve_operator() - user_vo_module = self.infrastructure_gateway.get_user_vo_module() - return user_vo_module.CurrentUserModel( - permissions=[], - roles=['admin'], - user=user_vo_module.UserInfoModel( - user_id=1, - user_name=operator, - nick_name=operator, - user_type='00', - status='0', - del_flag='0', - ), - ) - - @staticmethod - def normalize_table_names(table_names: list[str]) -> list[str]: - """ - 规范化表名列表。 - - :param table_names: 原始表名列表 - :return: 规范化后的表名列表 - """ - return [table_name.strip() for table_name in table_names if table_name.strip()] - - @staticmethod - def serialize_gen_item(item: Any) -> dict[str, Any]: - """ - 序列化单个代码生成表记录。 - - :param item: 原始记录对象 - :return: 可输出的字典 - """ - if hasattr(item, 'model_dump'): - return dict(item.model_dump(by_alias=True, exclude_none=True)) - return dict(item) - - def serialize_gen_items(self, items: list[Any]) -> list[dict[str, Any]]: - """ - 序列化代码生成表记录列表。 - - :param items: 原始记录列表 - :return: 序列化后的字典列表 - """ - return [self.serialize_gen_item(item) for item in items] - - def parse_create_table_sql(self, sql: str) -> tuple[list[Any], list[str]]: - """ - 解析建表 SQL 并提取建表语句信息。 - - :param sql: 原始 SQL 文本 - :return: SQL AST 列表与建表表名列表 - :raises ValueError: SQL 非法时抛出异常 - """ - sqlglot_module = self.infrastructure_gateway.get_sqlglot_module() - expressions_module = self.infrastructure_gateway.get_sqlglot_expressions_module() - database_config = self.infrastructure_gateway.get_database_config() - sql_statements = sqlglot_module.parse(sql, dialect=database_config.sqlglot_parse_dialect) - has_create = any(isinstance(sql_statement, expressions_module.Create) for sql_statement in sql_statements) - has_forbidden_keyword = any( - isinstance( - sql_statement, - ( - expressions_module.Add, - expressions_module.Alter, - expressions_module.Delete, - expressions_module.Drop, - expressions_module.Insert, - expressions_module.TruncateTable, - expressions_module.Update, - ), - ) - for sql_statement in sql_statements - ) - if not has_create or has_forbidden_keyword: - raise ValueError('建表语句不合法,仅允许 CREATE TABLE 语句') - - table_names = [ - sql_statement.find(expressions_module.Table).name - for sql_statement in sql_statements - if isinstance(sql_statement, expressions_module.Create) - ] - if not table_names: - raise ValueError('未解析到建表表名') - return sql_statements, table_names - - @staticmethod - def resolve_sql_text(sql: str, sql_file: str) -> str: - """ - 解析命令输入中的 SQL 文本。 - - :param sql: 直接传入的 SQL 文本 - :param sql_file: SQL 文件路径 - :return: 最终 SQL 文本 - :raises ValueError: 参数非法时抛出异常 - """ - if bool(sql.strip()) == bool(sql_file.strip()): - raise ValueError('必须且只能传入 --sql 或 --sql-file 其中一种方式') - if sql.strip(): - return sql.strip() - - sql_path = Path(sql_file).expanduser().resolve() - if not sql_path.is_file(): - raise ValueError(f'SQL 文件不存在:{sql_path}') - return sql_path.read_text(encoding='utf-8').strip() - - @staticmethod - def resolve_output_file_path(target_file: str) -> str: - """ - 解析导出文件绝对路径。 - - :param target_file: 原始导出文件路径 - :return: 绝对路径字符串 - """ - return str(Path(target_file).expanduser().resolve()) - - @staticmethod - def write_export_zip(target_file: str, zip_bytes: bytes) -> str: - """ - 将导出的 zip 数据写入目标文件。 - - :param target_file: 目标文件路径 - :param zip_bytes: zip 二进制内容 - :return: 实际写入的绝对路径 - """ - target_path = Path(target_file).expanduser().resolve() - target_path.parent.mkdir(parents=True, exist_ok=True) - target_path.write_bytes(zip_bytes) - return str(target_path) - - def build_list_payload( - self, - result: Any, - *, - filters: dict[str, Any], - paged: bool, - ) -> dict[str, Any]: - """ - 统一构建代码生成列表返回结构。 - - :param result: 原始结果对象 - :param filters: 查询过滤条件 - :param paged: 是否分页 - :return: 可输出结果 - """ - if paged and isinstance(result, self.infrastructure_gateway.get_page_model()): - page_payload = result.model_dump(by_alias=True) - page_payload['rows'] = self.serialize_gen_items(page_payload.get('rows', [])) - return {'ok': True, 'filters': filters, 'page': page_payload} - - items = self.serialize_gen_items(result) - return {'ok': True, 'filters': filters, 'count': len(items), 'items': items} diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/job/__init__.py deleted file mode 100644 index 214da66..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import JOB_RUNTIME, JobRuntimeService - -__all__ = ['JOB_RUNTIME', 'JobRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/job/gateway.py deleted file mode 100644 index b2491d4..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/gateway.py +++ /dev/null @@ -1,74 +0,0 @@ -from importlib import import_module -from typing import Any - - -class JobInfrastructureGateway: - """ - 定时任务基础设施网关。 - - 该对象负责延迟加载数据库会话、Redis、调度器和任务服务依赖, - 供定时任务运行时 facade 与其协作对象统一复用。 - """ - - @staticmethod - def get_page_model() -> Any: - """ - 获取分页模型类型。 - - :return: 分页模型类型 - """ - return import_module('common.vo').PageModel - - @staticmethod - def get_async_session_local() -> Any: - """ - 获取异步数据库会话工厂。 - - :return: 异步数据库会话工厂 - """ - return import_module('config.database').AsyncSessionLocal - - @staticmethod - def get_redis_util() -> Any: - """ - 获取 Redis 工具类。 - - :return: Redis 工具类 - """ - return import_module('config.get_redis').RedisUtil - - @staticmethod - def get_scheduler_util() -> Any: - """ - 获取调度器工具类。 - - :return: 调度器工具类 - """ - return import_module('config.get_scheduler').SchedulerUtil - - @staticmethod - def get_job_vo_module() -> Any: - """ - 获取定时任务 VO 模块。 - - :return: 定时任务 VO 模块 - """ - return import_module('module_admin.entity.vo.job_vo') - - @staticmethod - def get_job_service() -> Any: - """ - 获取定时任务服务类。 - - :return: 定时任务服务类 - """ - return import_module('module_admin.service.job_service').JobService - - @staticmethod - def get_job_log_service() -> Any: - """ - 获取定时任务日志服务类。 - - :return: 定时任务日志服务类 - """ - return import_module('module_admin.service.job_log_service').JobLogService diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/job/service.py deleted file mode 100644 index 9cf8e62..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/service.py +++ /dev/null @@ -1,246 +0,0 @@ -from typing import Any - -from cli.exit_codes import DATABASE_ERROR, RUNTIME_ERROR - -from .gateway import JobInfrastructureGateway -from .support import JobDomainSupport, JobSchedulerSupport - - -class JobRuntimeService: - """ - 定时任务运行时服务。 - - 该服务作为定时任务运行时 facade,对外统一暴露任务列表、详情、日志、 - 执行一次、暂停与恢复入口。 - - :param infrastructure_gateway: 定时任务基础设施网关 - :param domain_support: 定时任务领域支持对象 - :param scheduler_support: 定时任务调度上下文支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: JobInfrastructureGateway | None = None, - domain_support: JobDomainSupport | None = None, - scheduler_support: JobSchedulerSupport | None = None, - ) -> None: - """ - 初始化定时任务运行时服务。 - - :param infrastructure_gateway: 定时任务基础设施网关 - :param domain_support: 定时任务领域支持对象 - :param scheduler_support: 定时任务调度上下文支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or JobInfrastructureGateway() - self.domain_support = domain_support or JobDomainSupport(self.infrastructure_gateway) - self.scheduler_support = scheduler_support or JobSchedulerSupport(self.infrastructure_gateway) - - async def list_jobs( - self, - *, - job_name: str = '', - job_group: str = '', - status: str | None = None, - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 查询定时任务列表。 - - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 状态过滤条件 - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 定时任务列表结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - job_vo_module = self.infrastructure_gateway.get_job_vo_module() - job_service = self.infrastructure_gateway.get_job_service() - query_model = job_vo_module.JobPageQueryModel( - jobName=job_name or None, - jobGroup=job_group or None, - status=status, - pageNum=page_num, - pageSize=page_size, - ) - try: - async with async_session_local() as session: - result = await job_service.get_job_list_services(session, query_model, is_page=paged) - except Exception as exc: - return {'ok': False, 'message': '读取定时任务列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - filters = self.domain_support.build_filters( - job_name=job_name, - job_group=job_group, - status=status, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - return self.domain_support.build_list_payload(result, filters=filters, paged=paged) - - async def get_job_detail(self, job_id: int) -> dict[str, Any]: - """ - 读取单个定时任务详情。 - - :param job_id: 任务 ID - :return: 定时任务详情结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - job_service = self.infrastructure_gateway.get_job_service() - try: - async with async_session_local() as session: - job_model = await job_service.job_detail_services(session, job_id) - except Exception as exc: - return {'ok': False, 'message': '读取定时任务详情失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - job_payload = self.domain_support.serialize_job_item(job_model) - if not job_payload.get('jobId'): - return { - 'ok': False, - 'message': f'定时任务不存在:{job_id}', - 'jobId': job_id, - 'exit_code': RUNTIME_ERROR, - } - return {'ok': True, 'job': job_payload} - - async def list_job_logs( - self, - *, - job_name: str = '', - job_group: str = '', - status: str | None = None, - begin_date: str = '', - end_date: str = '', - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 查询定时任务执行日志列表。 - - :param job_name: 任务名称过滤条件 - :param job_group: 任务组过滤条件 - :param status: 执行状态过滤条件 - :param begin_date: 开始日期,格式 `YYYY-MM-DD` - :param end_date: 结束日期,格式 `YYYY-MM-DD` - :param paged: 是否启用分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 定时任务日志列表结果 - """ - async_session_local = self.infrastructure_gateway.get_async_session_local() - job_log_service = self.infrastructure_gateway.get_job_log_service() - job_vo_module = self.infrastructure_gateway.get_job_vo_module() - query_model = job_vo_module.JobLogPageQueryModel( - jobName=job_name or None, - jobGroup=job_group or None, - status=status, - beginTime=begin_date or None, - endTime=end_date or None, - pageNum=page_num, - pageSize=page_size, - ) - try: - async with async_session_local() as session: - result = await job_log_service.get_job_log_list_services(session, query_model, is_page=paged) - except Exception as exc: - return {'ok': False, 'message': '读取定时任务日志列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR} - - filters = self.domain_support.build_filters( - job_name=job_name, - job_group=job_group, - status=status, - begin_date=begin_date, - end_date=end_date, - paged=paged, - page_num=page_num, - page_size=page_size, - ) - return self.domain_support.build_list_payload(result, filters=filters, paged=paged) - - async def run_with_scheduler_context( - self, - operation: str, - job_id: int, - *, - status: str | None = None, - ) -> dict[str, Any]: - """ - 在调度器上下文中执行任务操作。 - - :param operation: 操作名称 - :param job_id: 任务ID - :param status: 目标状态 - :return: 任务操作结果 - """ - operation_metadata = self.domain_support.build_job_operation_metadata(operation) - redis = None - async_session_local = self.infrastructure_gateway.get_async_session_local() - redis_util = self.infrastructure_gateway.get_redis_util() - scheduler_util = self.infrastructure_gateway.get_scheduler_util() - job_vo_module = self.infrastructure_gateway.get_job_vo_module() - job_service = self.infrastructure_gateway.get_job_service() - try: - async with async_session_local() as session: - redis = await redis_util.create_redis_pool(log_enabled=False) - await scheduler_util.init_system_scheduler(redis) - - if operation == 'run-once': - result = await job_service.execute_job_once_services(session, job_vo_module.JobModel(jobId=job_id)) - elif operation in {'pause', 'resume'}: - result = await job_service.edit_job_services( - session, - job_vo_module.EditJobModel(jobId=job_id, status=status, type='status'), - ) - else: - raise ValueError(f'不支持的任务操作:{operation}') - except Exception as exc: - return {'ok': False, 'message': '执行定时任务操作失败', 'error': str(exc), 'exit_code': 22} - finally: - await self.scheduler_support.close_scheduler_context(redis) - - return { - 'ok': bool(result.is_success), - 'jobId': job_id, - 'operation': operation, - 'operationLabel': operation_metadata['operationLabel'], - 'targetStatus': status, - 'message': operation_metadata['successMessage'] if result.is_success else '定时任务操作执行失败', - 'serviceMessage': result.message, - } - - async def run_job_once(self, job_id: int) -> dict[str, Any]: - """ - 执行一次指定定时任务。 - - :param job_id: 任务ID - :return: 执行结果 - """ - return await self.run_with_scheduler_context('run-once', job_id) - - async def pause_job(self, job_id: int) -> dict[str, Any]: - """ - 暂停指定定时任务。 - - :param job_id: 任务ID - :return: 暂停结果 - """ - return await self.run_with_scheduler_context('pause', job_id, status='1') - - async def resume_job(self, job_id: int) -> dict[str, Any]: - """ - 恢复指定定时任务。 - - :param job_id: 任务ID - :return: 恢复结果 - """ - return await self.run_with_scheduler_context('resume', job_id, status='0') - - -JOB_RUNTIME = JobRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/job/support.py deleted file mode 100644 index 19578b2..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/job/support.py +++ /dev/null @@ -1,173 +0,0 @@ -from typing import Any - -from .gateway import JobInfrastructureGateway - - -class JobDomainSupport: - """ - 定时任务领域支持对象。 - - 该对象负责任务序列化、分页结果规整和任务操作元数据定义, - 避免主运行时服务继续承载细碎领域规则。 - - :param infrastructure_gateway: 定时任务基础设施网关 - """ - - def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None: - """ - 初始化定时任务领域支持对象。 - - :param infrastructure_gateway: 定时任务基础设施网关 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - - @staticmethod - def serialize_job_item(job_item: Any) -> dict[str, Any]: - """ - 序列化单个定时任务模型。 - - :param job_item: 原始定时任务模型或字典 - :return: 可输出的定时任务字典 - """ - if hasattr(job_item, 'model_dump'): - return dict(job_item.model_dump(by_alias=True, exclude_none=True)) - return dict(job_item) - - def serialize_job_items(self, job_items: list[dict[str, Any]]) -> list[dict[str, Any]]: - """ - 序列化任务列表结果。 - - :param job_items: 原始任务列表 - :return: 可输出的任务列表 - """ - return [self.serialize_job_item(item) for item in job_items] - - @staticmethod - def build_job_operation_metadata(operation: str) -> dict[str, str]: - """ - 构建任务操作的统一元数据。 - - :param operation: 操作名称 - :return: 操作元数据字典 - :raises ValueError: 操作名称不受支持时抛出异常 - """ - metadata_mapping = { - 'run-once': { - 'operationLabel': '执行一次任务', - 'successMessage': '定时任务已触发一次执行', - }, - 'pause': { - 'operationLabel': '暂停任务', - 'successMessage': '定时任务已暂停', - }, - 'resume': { - 'operationLabel': '恢复任务', - 'successMessage': '定时任务已恢复', - }, - } - if operation not in metadata_mapping: - raise ValueError(f'不支持的任务操作:{operation}') - return metadata_mapping[operation] - - def build_filters( - self, - *, - job_name: str = '', - job_group: str = '', - status: str | None = None, - begin_date: str = '', - end_date: str = '', - paged: bool = False, - page_num: int = 1, - page_size: int = 20, - ) -> dict[str, Any]: - """ - 构建任务列表或日志查询过滤条件。 - - :param job_name: 任务名称 - :param job_group: 任务分组 - :param status: 状态 - :param begin_date: 开始日期 - :param end_date: 结束日期 - :param paged: 是否分页 - :param page_num: 页码 - :param page_size: 每页数量 - :return: 过滤条件字典 - """ - filters = { - 'jobName': job_name, - 'jobGroup': job_group, - 'status': status, - 'paged': paged, - 'pageNum': page_num, - 'pageSize': page_size, - } - if begin_date or end_date: - filters['beginDate'] = begin_date - filters['endDate'] = end_date - return filters - - def build_list_payload( - self, - result: Any, - *, - filters: dict[str, Any], - paged: bool, - ) -> dict[str, Any]: - """ - 统一构建任务列表或日志列表返回结构。 - - :param result: 原始结果对象 - :param filters: 查询过滤条件 - :param paged: 是否分页 - :return: 可输出结果 - """ - if paged and isinstance(result, self.infrastructure_gateway.get_page_model()): - page_payload = result.model_dump(by_alias=True) - page_payload['rows'] = self.serialize_job_items(page_payload.get('rows', [])) - return {'ok': True, 'filters': filters, 'page': page_payload} - - items = self.serialize_job_items(result) - return {'ok': True, 'filters': filters, 'count': len(items), 'items': items} - - -class JobSchedulerSupport: - """ - 定时任务调度上下文支持对象。 - - 该对象负责统一关闭调度器上下文资源,避免调度型操作在 facade 中 - 持续堆叠资源清理细节。 - - :param infrastructure_gateway: 定时任务基础设施网关 - """ - - def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None: - """ - 初始化定时任务调度上下文支持对象。 - - :param infrastructure_gateway: 定时任务基础设施网关 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - - async def close_scheduler_context(self, redis: Any) -> None: - """ - 安全关闭任务调度相关资源。 - - :param redis: Redis 客户端实例 - :return: None - """ - scheduler_util = self.infrastructure_gateway.get_scheduler_util() - try: - await scheduler_util.close_system_scheduler() - except Exception: - pass - - if redis is None: - return - - try: - await redis.close() - except Exception: - pass diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/__init__.py deleted file mode 100644 index 9c60709..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .service import OPERATIONS_RUNTIME, OperationsRuntimeService - -__all__ = ['OPERATIONS_RUNTIME', 'OperationsRuntimeService'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/gateway.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/gateway.py deleted file mode 100644 index 261c202..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/gateway.py +++ /dev/null @@ -1,65 +0,0 @@ -from importlib import import_module -from typing import Any - - -class OperationsInfrastructureGateway: - """ - 运维基础设施网关。 - - 该对象负责延迟加载 Redis、调度器、服务器监控与系统工具依赖, - 供运维运行时 facade 和其协作对象统一复用。 - """ - - @staticmethod - def get_redis_util() -> Any: - """ - 获取 Redis 工具类。 - - :return: Redis 工具类 - """ - return import_module('config.get_redis').RedisUtil - - @staticmethod - def get_redis_error_class() -> type[Exception]: - """ - 获取 Redis 异常类型。 - - :return: Redis 异常类型 - """ - return import_module('redis.exceptions').RedisError - - @staticmethod - def get_scheduler_util() -> Any: - """ - 获取调度器工具类。 - - :return: 调度器工具类 - """ - return import_module('config.get_scheduler').SchedulerUtil - - @staticmethod - def get_server_service() -> Any: - """ - 获取服务器监控服务类。 - - :return: 服务器监控服务类 - """ - return import_module('module_admin.service.server_service').ServerService - - @staticmethod - def get_psutil_module() -> Any: - """ - 获取 `psutil` 模块。 - - :return: `psutil` 模块 - """ - return import_module('psutil') - - @staticmethod - def get_bytes2human() -> Any: - """ - 获取字节数转可读文本函数。 - - :return: `bytes2human` 函数 - """ - return import_module('utils.common_util').bytes2human diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/service.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/service.py deleted file mode 100644 index cfff10c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/service.py +++ /dev/null @@ -1,132 +0,0 @@ -import socket -from typing import Any - -from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR, SCHEDULER_ERROR - -from .gateway import OperationsInfrastructureGateway -from .support import ( - OperationsDependencyInspector, - OperationsServerInfoSupport, -) - - -class OperationsRuntimeService: - """ - 运维运行时服务。 - - 该服务作为运维运行时 facade,对外统一暴露依赖版本检查、Redis 探活、 - 调度同步以及服务器运行时信息采集入口。 - - :param infrastructure_gateway: 运维基础设施网关 - :param dependency_inspector: 运维依赖检查器 - :param server_info_support: 服务器信息支持对象 - """ - - def __init__( - self, - *, - infrastructure_gateway: OperationsInfrastructureGateway | None = None, - dependency_inspector: OperationsDependencyInspector | None = None, - server_info_support: OperationsServerInfoSupport | None = None, - ) -> None: - """ - 初始化运维运行时服务。 - - :param infrastructure_gateway: 运维基础设施网关 - :param dependency_inspector: 运维依赖检查器 - :param server_info_support: 服务器信息支持对象 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway or OperationsInfrastructureGateway() - self.dependency_inspector = dependency_inspector or OperationsDependencyInspector() - self.server_info_support = server_info_support or OperationsServerInfoSupport(self.infrastructure_gateway) - - def get_dependency_versions(self, *, include_dev: bool = False) -> dict[str, Any]: - """ - 读取 CLI 和后端运行所依赖的核心 Python 包版本。 - - :param include_dev: 是否附带开发阶段依赖 - :return: 依赖检查结果 - """ - return self.dependency_inspector.inspect(include_dev=include_dev) - - async def ping_redis(self) -> dict[str, Any]: - """ - 检查 Redis 连通性。 - - :return: Redis 检查结果 - """ - redis_util = self.infrastructure_gateway.get_redis_util() - redis_error = self.infrastructure_gateway.get_redis_error_class() - redis = await redis_util.create_redis_pool(log_enabled=False) - try: - await redis.ping() - return {'ok': True, 'message': 'Redis连接成功'} - except redis_error as exc: - return {'ok': False, 'message': 'Redis连接失败', 'error': str(exc), 'exit_code': REDIS_ERROR} - finally: - await redis.close() - - async def sync_jobs(self) -> dict[str, Any]: - """ - 同步调度任务配置。 - - :return: 任务同步执行结果 - """ - redis = None - scheduler_util = None - try: - redis_util = self.infrastructure_gateway.get_redis_util() - scheduler_util = self.infrastructure_gateway.get_scheduler_util() - redis = await redis_util.create_redis_pool(log_enabled=False) - await scheduler_util.init_system_scheduler(redis) - await scheduler_util.request_scheduler_sync() - return { - 'ok': True, - 'operation': 'sync', - 'operationLabel': '同步调度配置', - 'schedulerSyncRequested': True, - 'message': '调度配置同步请求已发送', - 'isLeader': scheduler_util._is_leader, - } - except Exception as exc: - return { - 'ok': False, - 'operation': 'sync', - 'operationLabel': '同步调度配置', - 'schedulerSyncRequested': False, - 'message': '调度配置同步失败', - 'error': str(exc), - 'exit_code': SCHEDULER_ERROR, - } - finally: - if scheduler_util is not None: - try: - await scheduler_util.close_system_scheduler() - except Exception: - pass - if redis is not None: - try: - await redis.close() - except Exception: - pass - - async def get_server_info(self) -> dict[str, Any]: - """ - 获取服务器运行信息。 - - :return: 服务器运行信息字典 - """ - try: - server_service = self.infrastructure_gateway.get_server_service() - return {'ok': True, 'server': (await server_service.get_server_monitor_info()).model_dump(by_alias=True)} - except socket.gaierror: - try: - return {'ok': True, 'server': self.server_info_support.build_server_info_fallback()} - except Exception as exc: - return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR} - except Exception as exc: - return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR} - - -OPERATIONS_RUNTIME = OperationsRuntimeService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/support.py b/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/support.py deleted file mode 100644 index f891c14..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/runtime/ops/support.py +++ /dev/null @@ -1,241 +0,0 @@ -import os -import platform -import socket -import time -from dataclasses import dataclass -from importlib import metadata -from typing import Any - -from cli.exit_codes import RUNTIME_ERROR - -from .gateway import OperationsInfrastructureGateway - - -@dataclass(frozen=True) -class OperationsDependencySpec: - """ - 运维依赖检查项定义。 - - :param package_name: CLI 展示用包名 - :param distribution: Python 分发名称 - :param required: 是否为必需依赖 - :param version: 显式版本,存在时跳过环境读取 - """ - - package_name: str - distribution: str - required: bool - version: str | None = None - - -class OperationsDependencyInspector: - """ - 运维依赖检查器。 - - 该对象负责核心依赖声明、版本探测与依赖检查结果构建, - 使运行时 facade 不再直接维护细碎依赖定义。 - """ - - @staticmethod - def read_package_version(distribution_name: str) -> str | None: - """ - 读取指定 Python 包的已安装版本号。 - - :param distribution_name: Python 包分发名称 - :return: 已安装版本号,未安装时返回 `None` - """ - try: - return metadata.version(distribution_name) - except metadata.PackageNotFoundError: - return None - - @staticmethod - def build_dependency_specs(*, include_dev: bool = False) -> list[OperationsDependencySpec]: - """ - 构建当前依赖检查项列表。 - - :param include_dev: 是否附带开发阶段依赖 - :return: 依赖检查项列表 - """ - dependency_specs = [ - OperationsDependencySpec('python', '', True, platform.python_version()), - OperationsDependencySpec('fastapi', 'fastapi', True), - OperationsDependencySpec('uvicorn', 'uvicorn', True), - OperationsDependencySpec('sqlalchemy', 'SQLAlchemy', True), - OperationsDependencySpec('alembic', 'alembic', True), - OperationsDependencySpec('redis', 'redis', True), - OperationsDependencySpec('pydantic', 'pydantic', True), - OperationsDependencySpec('pydantic-settings', 'pydantic-settings', True), - OperationsDependencySpec('typer', 'typer', True), - ] - if include_dev: - dependency_specs.extend( - [ - OperationsDependencySpec('pytest', 'pytest', False), - OperationsDependencySpec('ruff', 'ruff', False), - ] - ) - return dependency_specs - - def inspect(self, *, include_dev: bool = False) -> dict[str, Any]: - """ - 读取 CLI 和后端运行所依赖的核心 Python 包版本。 - - :param include_dev: 是否附带开发阶段依赖 - :return: 依赖检查结果 - """ - packages: dict[str, dict[str, Any]] = {} - missing_required: list[str] = [] - for dependency_spec in self.build_dependency_specs(include_dev=include_dev): - version = dependency_spec.version or self.read_package_version(dependency_spec.distribution) - installed = bool(version) - package_payload = { - 'installed': installed, - 'version': version or '', - 'required': dependency_spec.required, - } - if dependency_spec.distribution: - package_payload['distribution'] = dependency_spec.distribution - packages[dependency_spec.package_name] = package_payload - if dependency_spec.required and not installed: - missing_required.append(dependency_spec.package_name) - - if missing_required: - return { - 'ok': False, - 'message': '存在缺失的核心运行依赖', - 'missingRequired': missing_required, - 'includeDev': include_dev, - 'packages': packages, - 'exit_code': RUNTIME_ERROR, - } - - return { - 'ok': True, - 'message': '核心运行依赖已安装', - 'missingRequired': [], - 'includeDev': include_dev, - 'packages': packages, - } - - -class OperationsServerInfoSupport: - """ - 运维服务器信息支持对象。 - - 该对象负责 CLI 兜底服务器信息构建与 IP 解析逻辑, - 避免主运行时服务继续承载大量平台采集细节。 - - :param infrastructure_gateway: 运维基础设施网关 - """ - - def __init__(self, infrastructure_gateway: OperationsInfrastructureGateway) -> None: - """ - 初始化服务器信息支持对象。 - - :param infrastructure_gateway: 运维基础设施网关 - :return: None - """ - self.infrastructure_gateway = infrastructure_gateway - - def resolve_server_ip(self, hostname: str) -> str: - """ - 解析服务器 IP,优先使用主机名解析,失败时回退到网卡地址。 - - :param hostname: 当前主机名 - :return: 可用的 IPv4 地址 - """ - psutil = self.infrastructure_gateway.get_psutil_module() - try: - resolved_ip = socket.gethostbyname(hostname) - if resolved_ip: - return resolved_ip - except OSError: - pass - - for interface_addresses in psutil.net_if_addrs().values(): - for address_info in interface_addresses: - if ( - address_info.family == socket.AF_INET - and address_info.address - and not address_info.address.startswith('127.') - ): - return address_info.address - - return '127.0.0.1' - - def build_server_info_fallback(self) -> dict[str, Any]: - """ - 构建 CLI 使用的服务器运行信息兜底数据。 - - :return: 与原服务输出结构兼容的服务器运行信息字典 - """ - psutil = self.infrastructure_gateway.get_psutil_module() - bytes2human = self.infrastructure_gateway.get_bytes2human() - cpu_num = psutil.cpu_count(logical=True) - cpu_usage_percent = psutil.cpu_times_percent() - memory_info = psutil.virtual_memory() - - hostname = socket.gethostname() - current_process = psutil.Process(os.getpid()) - start_time_stamp = current_process.create_time() - current_time_stamp = time.time() - difference = current_time_stamp - start_time_stamp - days = int(difference // (24 * 60 * 60)) - hours = int((difference % (24 * 60 * 60)) // (60 * 60)) - minutes = int((difference % (60 * 60)) // 60) - process_memory_info = current_process.memory_info() - - sys_files: list[dict[str, Any]] = [] - for partition in psutil.disk_partitions(): - try: - disk_usage = psutil.disk_usage(partition.mountpoint) - except Exception: - continue - - mountpoint = partition.mountpoint.replace('\\', '') - sys_files.append( - { - 'dirName': partition.device, - 'sysTypeName': partition.fstype, - 'typeName': f'本地固定磁盘({mountpoint})', - 'total': bytes2human(disk_usage.total), - 'used': bytes2human(disk_usage.used), - 'free': bytes2human(disk_usage.free), - 'usage': f'{disk_usage.percent}%', - } - ) - - return { - 'cpu': { - 'cpuNum': cpu_num, - 'used': cpu_usage_percent.user, - 'sys': cpu_usage_percent.system, - 'free': cpu_usage_percent.idle, - }, - 'mem': { - 'total': bytes2human(memory_info.total), - 'used': bytes2human(memory_info.used), - 'free': bytes2human(memory_info.free), - 'usage': memory_info.percent, - }, - 'sys': { - 'computerIp': self.resolve_server_ip(hostname), - 'computerName': platform.node(), - 'osArch': platform.machine(), - 'osName': platform.platform(), - 'userDir': os.getcwd(), - }, - 'py': { - 'name': current_process.name(), - 'version': platform.python_version(), - 'startTime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp)), - 'runTime': f'{days}天{hours}小时{minutes}分钟', - 'home': current_process.exe(), - 'total': bytes2human(memory_info.available), - 'used': bytes2human(process_memory_info.rss), - 'free': bytes2human(memory_info.available - process_memory_info.rss), - 'usage': round((process_memory_info.rss / memory_info.available) * 100, 2), - }, - 'sysFiles': sys_files, - } diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/__init__.py deleted file mode 100644 index a21e01d..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from cli.tui.commands import TUI_COMMAND_REGISTRATION - -__all__ = ['TUI_COMMAND_REGISTRATION'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/__init__.py deleted file mode 100644 index 5272c71..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from cli.tui.actions.bootstrap import TUI_ACTION_EXECUTION_SERVICE, TUI_ACTION_PRESENTATION_SERVICE, TUI_ACTION_REGISTRY -from cli.tui.actions.models import TuiActionResult, TuiActionSpec - -__all__ = [ - 'TUI_ACTION_EXECUTION_SERVICE', - 'TUI_ACTION_PRESENTATION_SERVICE', - 'TUI_ACTION_REGISTRY', - 'TuiActionResult', - 'TuiActionSpec', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/assembly.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/assembly.py deleted file mode 100644 index 1496f75..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/assembly.py +++ /dev/null @@ -1,103 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.builders import TuiActionSpecFactory -from cli.tui.actions.factories.cache import CacheActionTemplateFactory -from cli.tui.actions.factories.gen import GenActionTemplateFactory -from cli.tui.actions.factories.jobs import JobActionTemplateFactory -from cli.tui.actions.factories.static import StaticActionTemplateFactory -from cli.tui.actions.registry import TuiActionRegistry, TuiActionSlotResolver - - -@dataclass(frozen=True) -class TuiActionRegistryBuilder: - """ - TUI 动作注册表构建器。 - - 该对象负责装配各领域动作模板工厂,最终输出浏览页和详情页共用的 - 动作注册表,避免模板常量继续散落在模块底部。 - - :param jobs: 任务页动作模板工厂 - :param cache: 缓存页动作模板工厂 - :param gen: 代码生成页动作模板工厂 - :param static: 静态页面动作模板工厂 - :param spec_factory: 动作规格构建器 - """ - - jobs: JobActionTemplateFactory - cache: CacheActionTemplateFactory - gen: GenActionTemplateFactory - static: StaticActionTemplateFactory - spec_factory: TuiActionSpecFactory - - def build(self) -> TuiActionRegistry: - """ - 构建 TUI 动作注册表。 - - :return: 动作注册表 - """ - return TuiActionRegistry( - browser_resolvers={ - 'jobs': TuiActionSlotResolver( - slot_templates={ - 'primary': self.jobs.create_run_once_template(), - 'secondary': self.jobs.create_toggle_template(), - 'global': self.jobs.create_sync_template(), - }, - spec_factory=self.spec_factory, - ), - 'configs': TuiActionSlotResolver( - slot_templates={ - 'global': self.static.create_config_sync_template(), - }, - spec_factory=self.spec_factory, - ), - 'cache': TuiActionSlotResolver( - slot_templates={ - 'global': self.cache.create_clear_wizard_template(), - 'utility': self.cache.create_warmup_template(), - }, - spec_factory=self.spec_factory, - ), - 'gen': TuiActionSlotResolver( - slot_templates={ - 'primary': self.gen.create_export_wizard_template(), - 'secondary': self.gen.create_import_wizard_template(), - 'global': self.gen.create_export_dry_run_template(), - 'utility': self.gen.create_sync_db_template(), - }, - spec_factory=self.spec_factory, - ), - }, - detail_resolvers={ - 'app': TuiActionSlotResolver( - slot_templates={ - 'primary': self.static.create_app_run_template(), - 'global': self.static.create_app_run_wizard_template(), - 'utility': self.static.create_completion_install_template(), - }, - spec_factory=self.spec_factory, - ), - 'database': TuiActionSlotResolver( - slot_templates={ - 'global': self.static.create_db_upgrade_wizard_template(), - 'utility': self.static.create_db_init_dry_run_template(), - }, - spec_factory=self.spec_factory, - ), - 'ops': TuiActionSlotResolver( - slot_templates={ - 'primary': self.static.create_ops_ping_db_template(), - 'secondary': self.static.create_ops_ping_redis_template(), - 'global': self.static.create_prod_check_wizard_template(), - }, - spec_factory=self.spec_factory, - ), - 'crypto': TuiActionSlotResolver( - slot_templates={ - 'primary': self.static.create_crypto_keygen_template(), - 'global': self.static.create_crypto_rotate_dry_run_template(), - }, - spec_factory=self.spec_factory, - ), - }, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/bootstrap.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/bootstrap.py deleted file mode 100644 index a89c859..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/bootstrap.py +++ /dev/null @@ -1,31 +0,0 @@ -from cli.tui.actions.assembly import TuiActionRegistryBuilder -from cli.tui.actions.builders import TuiActionSpecFactory, TuiActionTemplateSupport -from cli.tui.actions.execution import TuiActionExecutionService -from cli.tui.actions.factories.cache import CacheActionTemplateFactory -from cli.tui.actions.factories.gen import GenActionTemplateFactory -from cli.tui.actions.factories.jobs import JobActionTemplateFactory -from cli.tui.actions.factories.static import StaticActionTemplateFactory -from cli.tui.actions.presentation import TuiActionPresentationService -from cli.tui.capabilities import TUI_CAPABILITY_REGISTRY - -_ACTION_SPEC_FACTORY = TuiActionSpecFactory() -TUI_ACTION_EXECUTION_SERVICE = TuiActionExecutionService() - -_ACTION_TEMPLATE_SUPPORT = TuiActionTemplateSupport(spec_factory=_ACTION_SPEC_FACTORY) -_JOB_ACTION_TEMPLATE_FACTORY = JobActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT) -_CACHE_ACTION_TEMPLATE_FACTORY = CacheActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT) -_GEN_ACTION_TEMPLATE_FACTORY = GenActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT) -_STATIC_ACTION_TEMPLATE_FACTORY = StaticActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT) -_ACTION_REGISTRY_BUILDER = TuiActionRegistryBuilder( - jobs=_JOB_ACTION_TEMPLATE_FACTORY, - cache=_CACHE_ACTION_TEMPLATE_FACTORY, - gen=_GEN_ACTION_TEMPLATE_FACTORY, - static=_STATIC_ACTION_TEMPLATE_FACTORY, - spec_factory=_ACTION_SPEC_FACTORY, -) -TUI_ACTION_REGISTRY = _ACTION_REGISTRY_BUILDER.build() - -TUI_ACTION_PRESENTATION_SERVICE = TuiActionPresentationService( - capability_registry=TUI_CAPABILITY_REGISTRY, - action_registry=TUI_ACTION_REGISTRY, -) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/builders.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/builders.py deleted file mode 100644 index 830a65d..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/builders.py +++ /dev/null @@ -1,329 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from cli.tui.actions.models import ( - ActionCommandBuilder, - ActionExecutionMode, - ActionSummaryBuilder, - ActionTextBuilder, - TuiActionSpec, -) -from cli.tui.copy import TUI_COPY -from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER - -if TYPE_CHECKING: - from cli.tui.adapters.models import BrowserRecordSnapshot - - -@dataclass(frozen=True) -class TuiActionSpecFactory: - """ - TUI 动作规格构建器。 - - 该对象负责统一构建动作预览文本与 `TuiActionSpec`, - 避免各动作函数继续手工拼接重复的预览和规格对象。 - """ - - @staticmethod - def parse_record_suffix(record: BrowserRecordSnapshot, prefix: str) -> str: - """ - 从记录键中提取指定前缀后的标识。 - - :param record: 浏览记录 - :param prefix: 键名前缀 - :return: 后缀标识 - """ - raw_key = str(record.key).strip() - token_prefix = f'{prefix}:' - if not raw_key.startswith(token_prefix): - return '' - return raw_key[len(token_prefix) :].strip() - - def build_command_preview_lines( - self, - *, - env: str, - summary_lines: list[str], - command_args: tuple[str, ...], - append_yes: bool = True, - consequence_text: str = TUI_COPY.build_action_consequence_text('preview'), - ) -> list[str]: - """ - 构建统一的动作预览文本。 - - :param env: 当前运行环境 - :param summary_lines: 业务摘要文本 - :param command_args: 对应 CLI 命令参数 - :param consequence_text: 结果影响说明 - :return: 预览文本 - """ - nested_cli_arguments = [ - *command_args, - f'--env={env}', - '--output=json', - ] - if append_yes: - nested_cli_arguments.append('--yes') - command = SHELL_TEXT_FORMATTER.format_shell_command( - NESTED_CLI_SUPPORT.build_nested_cli_command(*nested_cli_arguments) - ) - return [ - TUI_COPY.build_action_preview_title('summary'), - *summary_lines, - '', - TUI_COPY.build_action_preview_title('env'), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('target_env'), env), - '', - TUI_COPY.build_action_preview_title('command'), - command, - '', - TUI_COPY.build_action_preview_title('consequence'), - consequence_text, - ] - - def build_external_command_preview_lines( - self, - *, - env: str, - summary_lines: list[str], - command_args: tuple[str, ...], - append_yes: bool = True, - consequence_text: str = TUI_COPY.build_action_consequence_text('wizard'), - ) -> list[str]: - """ - 构建外部交互命令的预览文本。 - - :param env: 当前运行环境 - :param summary_lines: 业务摘要文本 - :param command_args: 对应 CLI 命令参数 - :param consequence_text: 结果影响说明 - :return: 预览文本 - """ - return self.build_command_preview_lines( - env=env, - summary_lines=summary_lines, - command_args=command_args, - append_yes=append_yes, - consequence_text=consequence_text, - ) - - def build_nested_json_action( - self, - *, - action_id: str, - label: str, - command_args: tuple[str, ...], - env: str, - summary_lines: list[str], - preview_title: str | None = None, - consequence_text: str = TUI_COPY.build_action_consequence_text('preview'), - append_yes: bool = True, - refresh_view: bool = True, - ) -> TuiActionSpec: - """ - 构建标准 nested JSON 动作。 - - :param action_id: 动作唯一标识 - :param label: 动作显示名称 - :param command_args: 对应 CLI 命令参数 - :param env: 当前运行环境 - :param summary_lines: 业务摘要文本 - :param preview_title: 预览标题 - :param consequence_text: 结果影响说明 - :param refresh_view: 执行后是否刷新视图 - :return: 动作定义 - """ - return TuiActionSpec( - action_id=action_id, - label=label, - command_args=command_args, - preview_title=preview_title or label, - preview_lines=self.build_command_preview_lines( - env=env, - summary_lines=summary_lines, - command_args=command_args, - append_yes=append_yes, - consequence_text=consequence_text, - ), - append_yes=append_yes, - refresh_view=refresh_view, - ) - - def build_external_action( - self, - *, - action_id: str, - label: str, - command_args: tuple[str, ...], - env: str, - summary_lines: list[str], - preview_title: str | None = None, - consequence_text: str = TUI_COPY.build_action_consequence_text('wizard'), - append_yes: bool = True, - refresh_view: bool = True, - ) -> TuiActionSpec: - """ - 构建标准外部交互动作。 - - :param action_id: 动作唯一标识 - :param label: 动作显示名称 - :param command_args: 对应 CLI 命令参数 - :param env: 当前运行环境 - :param summary_lines: 业务摘要文本 - :param preview_title: 预览标题 - :param consequence_text: 结果影响说明 - :param refresh_view: 执行后是否刷新视图 - :return: 动作定义 - """ - return TuiActionSpec( - action_id=action_id, - label=label, - command_args=command_args, - preview_title=preview_title or label, - preview_lines=self.build_external_command_preview_lines( - env=env, - summary_lines=summary_lines, - command_args=command_args, - append_yes=append_yes, - consequence_text=consequence_text, - ), - execution_mode='external', - append_yes=append_yes, - refresh_view=refresh_view, - ) - - -@dataclass(frozen=True) -class TuiActionTemplate: - """ - TUI 动作模板。 - - 该模板将命令参数构建、摘要构建与执行模式收敛为统一对象, - 使动作注册层可以直接声明“某个槽位对应哪类动作”。 - - :param action_id: 动作唯一标识 - :param label: 动作显示名称 - :param command_builder: 命令参数构建器 - :param summary_builder: 预览摘要构建器 - :param action_id_builder: 动态动作标识构建器 - :param label_builder: 动态动作标题构建器 - :param preview_title: 预览标题 - :param consequence_text: 结果影响说明 - :param execution_mode: 执行模式 - :param append_yes: 是否在命令执行与预览中自动追加 `--yes` - :param refresh_view: 执行后是否刷新视图 - :param preview_env_override: 预览中展示的环境名覆盖值 - """ - - action_id: str - label: str - command_builder: ActionCommandBuilder - summary_builder: ActionSummaryBuilder - action_id_builder: ActionTextBuilder | None = None - label_builder: ActionTextBuilder | None = None - preview_title: str | None = None - consequence_text: str = TUI_COPY.build_action_consequence_text('preview') - execution_mode: ActionExecutionMode = 'nested_json' - append_yes: bool = True - refresh_view: bool = True - preview_env_override: str | None = None - - def build( - self, - *, - record: BrowserRecordSnapshot | None, - env: str, - spec_factory: TuiActionSpecFactory, - ) -> TuiActionSpec | None: - """ - 根据当前页面上下文构建动作定义。 - - :param record: 当前选中记录 - :param env: 当前运行环境 - :param spec_factory: 动作规格构建器 - :return: 动作定义 - """ - command_args = self.command_builder(record, env) - if command_args is None: - return None - preview_env = self.preview_env_override or env - summary_lines = self.summary_builder(record, env) - action_id = self.action_id_builder(record, env) if self.action_id_builder is not None else self.action_id - label = self.label_builder(record, env) if self.label_builder is not None else self.label - if self.execution_mode == 'external': - return spec_factory.build_external_action( - action_id=action_id, - label=label, - command_args=command_args, - env=preview_env, - summary_lines=summary_lines, - preview_title=self.preview_title, - consequence_text=self.consequence_text, - append_yes=self.append_yes, - refresh_view=self.refresh_view, - ) - return spec_factory.build_nested_json_action( - action_id=action_id, - label=label, - command_args=command_args, - env=preview_env, - summary_lines=summary_lines, - preview_title=self.preview_title, - consequence_text=self.consequence_text, - append_yes=self.append_yes, - refresh_view=self.refresh_view, - ) - - -@dataclass(frozen=True) -class TuiActionTemplateSupport: - """ - TUI 动作模板共享构建支持。 - - 该对象集中封装多个动作模板都会复用的记录解析与摘要拼装逻辑, - 避免这些基础能力继续散落在模块级函数中。 - - :param spec_factory: 动作规格构建器 - """ - - spec_factory: TuiActionSpecFactory - - def build_scope_purpose_summary(self, scope: str, purpose: str) -> list[str]: - """ - 构建通用的作用范围与目的摘要。 - - :param scope: 作用范围说明 - :param purpose: 目的说明 - :return: 预览摘要 - """ - return [ - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('scope'), scope), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('purpose'), purpose), - ] - - def extract_job_id(self, record: BrowserRecordSnapshot | None) -> str | None: - """ - 从任务记录中提取任务 ID。 - - :param record: 当前记录 - :return: 任务 ID - """ - if record is None: - return None - job_id = self.spec_factory.parse_record_suffix(record, 'job') - return job_id if job_id.isdigit() else None - - @staticmethod - def require_record_title(record: BrowserRecordSnapshot | None) -> str | None: - """ - 提取记录标题作为命令目标。 - - :param record: 当前记录 - :return: 标题文本 - """ - if record is None: - return None - title = str(record.title).strip() - return title or None diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/execution.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/execution.py deleted file mode 100644 index cb15a63..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/execution.py +++ /dev/null @@ -1,121 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.models import TuiActionResult, TuiActionSpec -from cli.tui.copy import TUI_COPY -from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER - - -@dataclass(frozen=True) -class TuiActionExecutionService: - """ - TUI 动作执行服务。 - - 该对象负责统一封装 nested JSON 动作、外部交互动作 - 与结果文本行的收口逻辑。 - """ - - def execute(self, spec: TuiActionSpec, env: str) -> TuiActionResult: - """ - 执行指定 TUI 动作。 - - :param spec: 动作定义 - :param env: 当前运行环境 - :return: 动作执行结果 - """ - command_arguments = [ - *spec.command_args, - f'--env={env}', - '--output=json', - ] - if spec.append_yes: - command_arguments.append('--yes') - payload = NESTED_CLI_SUPPORT.run(*command_arguments, parse_json=True).payload - return TuiActionResult(spec=spec, payload=payload) - - def execute_external(self, spec: TuiActionSpec) -> TuiActionResult: - """ - 在当前终端中执行交互式 TUI 动作。 - - :param spec: 动作定义 - :return: 动作执行结果 - """ - completed = NESTED_CLI_SUPPORT.run_live(*spec.command_args) - if completed.returncode == 0: - message = '外部交互命令已执行完成' - else: - message = f'外部交互命令执行失败,退出码 {completed.returncode}' - return TuiActionResult( - spec=spec, - external_exit_code=completed.returncode, - external_message=message, - ) - - def build_result_lines(self, result: TuiActionResult) -> list[str]: - """ - 构建动作结果详情文本。 - - :param result: 动作执行结果 - :return: 结果文本行 - """ - payload = result.payload if isinstance(result.payload, dict) else {} - lines = [ - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('name'), - result.spec.label, - ), - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('outcome'), - TUI_COPY.build_action_result_field_label('success') - if result.ok - else TUI_COPY.build_action_result_field_label('fail'), - ), - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('summary'), - SHELL_TEXT_FORMATTER.truncate_text(result.message, 88), - ), - ] - service_message = str(payload.get('serviceMessage', '') or '').strip() - if service_message: - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('service'), - SHELL_TEXT_FORMATTER.truncate_text(service_message, 88), - ) - ) - if payload.get('hint'): - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('hint'), - SHELL_TEXT_FORMATTER.truncate_text(str(payload.get('hint', '') or ''), 88), - ) - ) - if payload.get('count') is not None: - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('count'), - str(payload.get('count')), - ) - ) - if payload.get('jobId') is not None: - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('job_id'), - str(payload.get('jobId')), - ) - ) - if result.external_exit_code is not None: - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('exit_code'), - str(result.external_exit_code), - ) - ) - operation_label = str(payload.get('operationLabel', '') or '').strip() - if operation_label and operation_label != result.spec.label: - lines.append( - TUI_COPY.build_action_result_message_line( - TUI_COPY.build_action_result_field_label('operation'), - SHELL_TEXT_FORMATTER.truncate_text(operation_label, 64), - ) - ) - return lines diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/cache.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/cache.py deleted file mode 100644 index 293b301..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/cache.py +++ /dev/null @@ -1,83 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport -from cli.tui.adapters.models import BrowserRecordSnapshot -from cli.tui.copy import TUI_COPY - - -@dataclass(frozen=True) -class CacheActionTemplateFactory: - """ - 缓存页动作模板工厂。 - - 该对象负责生成缓存浏览页相关动作模板,统一缓存清理向导和预热动作 - 的命令与预览摘要构建。 - - :param support: 动作模板共享构建支持 - """ - - support: TuiActionTemplateSupport - - def build_clear_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建缓存清理向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - cache_name = record.title if record is not None else '' - return ( - 'wizard', - 'cache-clear', - '--output=text', - f'--default-env={env}', - '--default-mode=cache-name', - f'--default-cache-name={cache_name}', - '--default-dry-run', - ) - - def build_clear_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建缓存清理向导摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del record, env - return self.support.build_scope_purpose_summary( - '当前环境缓存清理流程', - '进入向导后确认缓存名、键前缀和 dry-run 范围', - ) - - def create_warmup_template(self) -> TuiActionTemplate: - """ - 创建缓存预热动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='cache-warmup', - label=TUI_COPY.build_action_label('cache_warmup'), - command_builder=lambda record, env: ('cache', 'warmup'), - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('cache_warmup'), - TUI_COPY.build_action_purpose_label('cache_warmup'), - ), - ) - - def create_clear_wizard_template(self) -> TuiActionTemplate: - """ - 创建缓存清理向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-cache-clear', - label=TUI_COPY.build_action_label('cache_clear_wizard'), - command_builder=self.build_clear_command, - summary_builder=self.build_clear_summary, - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/gen.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/gen.py deleted file mode 100644 index 40bd68b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/gen.py +++ /dev/null @@ -1,254 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport -from cli.tui.adapters.models import BrowserRecordSnapshot -from cli.tui.copy import TUI_COPY - - -@dataclass(frozen=True) -class GenActionTemplateFactory: - """ - 代码生成页动作模板工厂。 - - 该对象负责生成代码生成浏览页相关动作模板,统一记录标题提取、 - 导出导入向导和表结构同步动作的构建逻辑。 - - :param support: 动作模板共享构建支持 - """ - - support: TuiActionTemplateSupport - - def build_export_wizard_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建代码生成导出向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - table_name = self.support.require_record_title(record) - if table_name is None: - return None - return ( - 'wizard', - 'gen-export', - '--output=text', - f'--default-env={env}', - f'--default-table-names={table_name}', - '--default-mode=zip', - '--default-dry-run', - ) - - def build_import_wizard_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建代码生成导入向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - table_name = self.support.require_record_title(record) - if table_name is None: - return None - return ( - 'wizard', - 'gen-import', - '--output=text', - f'--default-env={env}', - f'--default-table-names={table_name}', - '--default-dry-run', - ) - - def build_export_dry_run_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建代码生成导出 dry-run 命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del env - table_name = self.support.require_record_title(record) - if table_name is None: - return None - return ('gen', 'export', table_name, '--dry-run', '--mode=zip') - - def build_sync_db_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建代码生成表结构同步命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del env - table_name = self.support.require_record_title(record) - if table_name is None: - return None - return ('gen', 'sync-db', table_name) - - @staticmethod - def build_export_wizard_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建代码生成导出向导摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - return [ - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('scope'), f'当前业务表 {record.title}' - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('purpose'), - '进入向导后选择业务表、输出目录和覆盖策略', - ), - ] - - @staticmethod - def build_import_wizard_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建代码生成导入向导摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - return [ - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('scope'), f'当前物理表 {record.title}' - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('purpose'), - '进入向导后确认物理表列表,并先执行 dry-run 导入预演', - ), - ] - - @staticmethod - def build_export_dry_run_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建代码生成导出预演摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - return [ - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('scope'), - TUI_COPY.build_action_scope_label('gen_export_dry_run'), - ), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('purpose'), - TUI_COPY.build_action_purpose_label('gen_export_dry_run'), - ), - ] - - @staticmethod - def build_sync_db_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建代码生成表结构同步摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - return [ - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('scope'), - TUI_COPY.build_action_scope_label('gen_sync_db'), - ), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('purpose'), - TUI_COPY.build_action_purpose_label('gen_sync_db'), - ), - ] - - def create_export_wizard_template(self) -> TuiActionTemplate: - """ - 创建代码生成导出向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-gen-export', - label=TUI_COPY.build_action_label('gen_export_wizard'), - command_builder=self.build_export_wizard_command, - summary_builder=self.build_export_wizard_summary, - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - refresh_view=False, - ) - - def create_import_wizard_template(self) -> TuiActionTemplate: - """ - 创建代码生成导入向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-gen-import', - label=TUI_COPY.build_action_label('gen_import_wizard'), - command_builder=self.build_import_wizard_command, - summary_builder=self.build_import_wizard_summary, - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - refresh_view=False, - ) - - def create_export_dry_run_template(self) -> TuiActionTemplate: - """ - 创建代码生成导出预演动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='gen-export-dry-run', - label=TUI_COPY.build_action_label('gen_export_dry_run'), - command_builder=self.build_export_dry_run_command, - summary_builder=self.build_export_dry_run_summary, - ) - - def create_sync_db_template(self) -> TuiActionTemplate: - """ - 创建代码生成表结构同步动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='gen-sync-db', - label=TUI_COPY.build_action_label('gen_sync_db'), - command_builder=self.build_sync_db_command, - summary_builder=self.build_sync_db_summary, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/jobs.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/jobs.py deleted file mode 100644 index 1e5032f..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/jobs.py +++ /dev/null @@ -1,171 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport -from cli.tui.adapters.models import BrowserRecordSnapshot -from cli.tui.copy import TUI_COPY - - -@dataclass(frozen=True) -class JobActionTemplateFactory: - """ - 任务页动作模板工厂。 - - 该对象负责生成任务浏览页相关动作模板,收口任务 ID 解析、 - 暂停/恢复切换和摘要拼装逻辑。 - - :param support: 动作模板共享构建支持 - """ - - support: TuiActionTemplateSupport - - def build_run_once_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建任务执行一次命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del env - job_id = self.support.extract_job_id(record) - if job_id is None: - return None - return ('job', 'run-once', job_id) - - def build_run_once_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建任务执行一次动作摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - job_id = self.support.extract_job_id(record) or '' - return [ - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job_id'), job_id), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - ] - - def build_toggle_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None: - """ - 构建任务暂停或恢复命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del env - if record is None: - return None - job_id = self.support.extract_job_id(record) - if job_id is None: - return None - if record.status == 'warn': - return ('job', 'resume', job_id) - return ('job', 'pause', job_id) - - def build_toggle_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]: - """ - 构建任务暂停或恢复动作摘要。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 预览摘要 - """ - del env - if record is None: - return [] - job_id = self.support.extract_job_id(record) or '' - label = ( - TUI_COPY.build_action_label('job_resume') - if record.status == 'warn' - else TUI_COPY.build_action_label('job_pause') - ) - return [ - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job_id'), job_id), - TUI_COPY.build_labeled_value_line( - TUI_COPY.build_action_preview_field_label('current_status'), - record.summary, - ), - TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('target_action'), label), - ] - - @staticmethod - def build_toggle_action_id(record: BrowserRecordSnapshot | None, env: str) -> str: - """ - 构建任务暂停或恢复动作标识。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 动作标识 - """ - del env - if record is not None and record.status == 'warn': - return 'job-resume' - return 'job-pause' - - @staticmethod - def build_toggle_label(record: BrowserRecordSnapshot | None, env: str) -> str: - """ - 构建任务暂停或恢复动作标题。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 动作标题 - """ - del env - if record is not None and record.status == 'warn': - return TUI_COPY.build_action_label('job_resume') - return TUI_COPY.build_action_label('job_pause') - - def create_run_once_template(self) -> TuiActionTemplate: - """ - 创建任务执行一次动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='job-run-once', - label=TUI_COPY.build_action_label('job_run_once'), - command_builder=self.build_run_once_command, - summary_builder=self.build_run_once_summary, - ) - - def create_toggle_template(self) -> TuiActionTemplate: - """ - 创建任务暂停或恢复动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='job-toggle', - label=TUI_COPY.build_action_label('job_pause'), - command_builder=self.build_toggle_command, - summary_builder=self.build_toggle_summary, - action_id_builder=self.build_toggle_action_id, - label_builder=self.build_toggle_label, - ) - - def create_sync_template(self) -> TuiActionTemplate: - """ - 创建任务同步动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='job-sync', - label=TUI_COPY.build_action_label('job_sync'), - command_builder=lambda record, env: ('job', 'sync'), - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('job_sync'), - TUI_COPY.build_action_purpose_label('job_sync'), - ), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/static.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/static.py deleted file mode 100644 index ba51ace..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/factories/static.py +++ /dev/null @@ -1,385 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport -from cli.tui.adapters.models import BrowserRecordSnapshot -from cli.tui.copy import TUI_COPY - - -@dataclass(frozen=True) -class StaticActionTemplateFactory: - """ - 静态页面动作模板工厂。 - - 该对象负责生成不依赖当前记录内容的页面动作模板,统一详情页和 - 部分浏览页的固定动作定义。 - - :param support: 动作模板共享构建支持 - """ - - support: TuiActionTemplateSupport - - @staticmethod - def build_job_sync_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建任务同步命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('job', 'sync') - - @staticmethod - def build_config_sync_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建配置同步命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('config', 'sync-cache') - - @staticmethod - def build_cache_warmup_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建缓存预热命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('cache', 'warmup') - - @staticmethod - def build_db_upgrade_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建数据库升级向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record - return ( - 'wizard', - 'db-upgrade', - '--output=text', - f'--default-env={env}', - '--default-revision=head', - '--default-dry-run', - ) - - @staticmethod - def build_db_init_dry_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建数据库初始化预演命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('db', 'init', '--dry-run') - - @staticmethod - def build_app_run_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建应用启动向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('wizard', 'app-run') - - @staticmethod - def build_completion_install_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建补全安装命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('completion', 'install', '--activate') - - @staticmethod - def build_prod_check_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建生产巡检向导命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record - return ( - 'wizard', - 'prod-check', - '--output=text', - f'--default-env={env}', - '--default-include-config', - ) - - @staticmethod - def build_crypto_rotate_dry_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建加密轮换预演命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('crypto', 'rotate', '--dry-run') - - @staticmethod - def build_app_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建应用直接启动命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record - return ('app', 'run', f'--env={env}') - - @staticmethod - def build_crypto_keygen_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建加密密钥生成命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record - return ('crypto', 'keygen', f'--env={env}', '--output=text') - - @staticmethod - def build_ops_ping_db_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建数据库探活命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('ops', 'ping-db') - - @staticmethod - def build_ops_ping_redis_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]: - """ - 构建 Redis 探活命令参数。 - - :param record: 当前记录 - :param env: 当前运行环境 - :return: 命令参数 - """ - del record, env - return ('ops', 'ping-redis') - - def create_config_sync_template(self) -> TuiActionTemplate: - """ - 创建配置同步动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='config-sync-cache', - label=TUI_COPY.build_action_label('config_sync'), - command_builder=self.build_config_sync_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('config_sync'), - TUI_COPY.build_action_purpose_label('config_sync'), - ), - ) - - def create_db_upgrade_wizard_template(self) -> TuiActionTemplate: - """ - 创建数据库升级向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-db-upgrade', - label=TUI_COPY.build_action_label('db_upgrade_wizard'), - command_builder=self.build_db_upgrade_wizard_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境数据库升级流程', - '进入向导后确认目标 revision、环境和 dry-run 预演', - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - ) - - def create_db_init_dry_run_template(self) -> TuiActionTemplate: - """ - 创建数据库初始化预演动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='db-init-dry-run', - label=TUI_COPY.build_action_label('db_init_dry_run'), - command_builder=self.build_db_init_dry_run_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('db_init_dry_run'), - TUI_COPY.build_action_purpose_label('db_init_dry_run'), - ), - append_yes=False, - ) - - def create_app_run_wizard_template(self) -> TuiActionTemplate: - """ - 创建应用启动向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-app-run', - label=TUI_COPY.build_action_label('app_run_wizard'), - command_builder=self.build_app_run_wizard_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境应用启动流程', - '进入向导后确认环境,并决定是否先执行启动前检查', - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - refresh_view=False, - ) - - def create_completion_install_template(self) -> TuiActionTemplate: - """ - 创建补全安装动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='completion-install', - label=TUI_COPY.build_action_label('completion_install'), - command_builder=self.build_completion_install_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('completion_install'), - TUI_COPY.build_action_purpose_label('completion_install'), - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('external'), - refresh_view=False, - preview_env_override='-', - ) - - def create_prod_check_wizard_template(self) -> TuiActionTemplate: - """ - 创建生产巡检向导动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='wizard-prod-check', - label=TUI_COPY.build_action_label('prod_check_wizard'), - command_builder=self.build_prod_check_wizard_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境生产巡检流程', - '进入向导后统一检查数据库、缓存和运行配置', - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - ) - - def create_crypto_rotate_dry_run_template(self) -> TuiActionTemplate: - """ - 创建加密轮换预演动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='crypto-rotate-dry-run', - label=TUI_COPY.build_action_label('crypto_rotate_dry_run'), - command_builder=self.build_crypto_rotate_dry_run_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境传输加密轮换预演', - '仅生成轮换辅助结果,不直接写入新密钥配置', - ), - append_yes=False, - ) - - def create_app_run_template(self) -> TuiActionTemplate: - """ - 创建应用直接启动动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='app-run', - label=TUI_COPY.build_action_label('app_run'), - command_builder=self.build_app_run_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境应用直接启动流程', - '确认后会在当前终端直接启动当前 FastAPI 应用,并持续占用终端会话', - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - refresh_view=False, - ) - - def create_crypto_keygen_template(self) -> TuiActionTemplate: - """ - 创建密钥生成动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='crypto-keygen', - label=TUI_COPY.build_action_label('crypto_keygen'), - command_builder=self.build_crypto_keygen_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - '当前环境新密钥生成流程', - '在终端中直接输出新密钥材料和环境变量补丁建议', - ), - execution_mode='external', - consequence_text=TUI_COPY.build_action_consequence_text('wizard'), - refresh_view=False, - ) - - def create_ops_ping_db_template(self) -> TuiActionTemplate: - """ - 创建数据库探活动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='ops-ping-db', - label=TUI_COPY.build_action_label('ops_ping_db'), - command_builder=self.build_ops_ping_db_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('ops_ping_db'), - TUI_COPY.build_action_purpose_label('ops_ping_db'), - ), - append_yes=False, - ) - - def create_ops_ping_redis_template(self) -> TuiActionTemplate: - """ - 创建 Redis 探活动作模板。 - - :return: 动作模板 - """ - return TuiActionTemplate( - action_id='ops-ping-redis', - label=TUI_COPY.build_action_label('ops_ping_redis'), - command_builder=self.build_ops_ping_redis_command, - summary_builder=lambda record, env: self.support.build_scope_purpose_summary( - TUI_COPY.build_action_scope_label('ops_ping_redis'), - TUI_COPY.build_action_purpose_label('ops_ping_redis'), - ), - append_yes=False, - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/models.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/models.py deleted file mode 100644 index 621bd5e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/models.py +++ /dev/null @@ -1,72 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass -from typing import Literal - -from cli.tui.adapters.models import TUI_ADAPTER_MODEL_RENDERER, BrowserRecordSnapshot - -ActionSlot = Literal['primary', 'secondary', 'global', 'utility'] -ActionExecutionMode = Literal['nested_json', 'external'] -ActionCommandBuilder = Callable[[BrowserRecordSnapshot | None, str], tuple[str, ...] | None] -ActionSummaryBuilder = Callable[[BrowserRecordSnapshot | None, str], list[str]] -ActionTextBuilder = Callable[[BrowserRecordSnapshot | None, str], str] - - -@dataclass(frozen=True) -class TuiActionSpec: - """ - TUI 低风险动作定义。 - - :param action_id: 动作唯一标识 - :param label: 动作显示名称 - :param command_args: 对应 CLI 命令参数 - :param preview_title: 预览弹窗标题 - :param preview_lines: 预览摘要文本 - :param append_yes: 执行 nested JSON 动作时是否自动追加 `--yes` - :param refresh_view: 执行完成后是否刷新当前页面 - """ - - action_id: str - label: str - command_args: tuple[str, ...] - preview_title: str - preview_lines: list[str] - execution_mode: ActionExecutionMode = 'nested_json' - append_yes: bool = True - refresh_view: bool = True - - -@dataclass(frozen=True) -class TuiActionResult: - """ - TUI 动作执行结果。 - - :param spec: 动作定义 - :param payload: CLI JSON 负载 - """ - - spec: TuiActionSpec - payload: dict[str, object] | None = None - external_exit_code: int | None = None - external_message: str | None = None - - @property - def ok(self) -> bool: - """ - 判断动作是否执行成功。 - - :return: 是否成功 - """ - if self.external_exit_code is not None: - return self.external_exit_code == 0 - return bool(isinstance(self.payload, dict) and self.payload.get('ok', False)) - - @property - def message(self) -> str: - """ - 获取动作结果摘要。 - - :return: 结果摘要 - """ - if self.external_message is not None: - return self.external_message - return TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(self.payload) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/presentation.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/presentation.py deleted file mode 100644 index e4cd232..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/presentation.py +++ /dev/null @@ -1,110 +0,0 @@ -from dataclasses import dataclass - -from cli.tui.actions.models import TuiActionResult -from cli.tui.actions.registry import TuiActionRegistry -from cli.tui.adapters.models import BrowserRecordSnapshot -from cli.tui.capabilities import TuiCapabilityRegistry -from cli.tui.copy import TUI_COPY -from cli.tui.keymaps import TUI_KEYMAP_REGISTRY -from cli.utils import SHELL_TEXT_FORMATTER - - -@dataclass(frozen=True) -class TuiActionPresentationService: - """ - TUI 动作展示服务。 - - 该对象负责动作提示、动作列表和动作结果通知等展示层文本构建, - 避免继续保留模块级桥接函数。 - """ - - capability_registry: TuiCapabilityRegistry - action_registry: TuiActionRegistry - - def build_browser_action_hint(self, view_key: str) -> str: - """ - 构建浏览页动作提示文本。 - - :param view_key: 当前页面视图标识 - :return: 动作提示文本 - """ - capability_labels = [ - f'[{capability.key}] {capability.hint_label}' - for capability in self.capability_registry.get_browser_capabilities(view_key) - ] - interaction_text = TUI_COPY.build_browser_action_hint_text( - view_key, TUI_KEYMAP_REGISTRY.browser_interaction_hint - ) - return TUI_COPY.build_capability_hint_text( - capability_labels, - interaction_text, - fallback='{interaction_hint}', - ) - - def build_detail_action_hint(self, view_key: str) -> str: - """ - 构建详情页动作提示文本。 - - :param view_key: 当前页面视图标识 - :return: 动作提示文本 - """ - capability_labels = [ - f'[{capability.key}] {capability.hint_label}' - for capability in self.capability_registry.get_detail_capabilities(view_key) - ] - interaction_text = TUI_COPY.build_detail_action_hint_text( - view_key, TUI_KEYMAP_REGISTRY.browser_interaction_hint - ) - return TUI_COPY.build_capability_hint_text( - capability_labels, - interaction_text, - fallback='{interaction_hint}', - ) - - def build_browser_action_lines( - self, - *, - view_key: str, - record: BrowserRecordSnapshot | None, - env: str, - ) -> list[str]: - """ - 构建当前页面可执行动作列表。 - - :param view_key: 当前页面视图标识 - :param record: 当前选中记录 - :param env: 当前运行环境 - :return: 动作文本行 - """ - lines: list[str] = [] - for capability in self.capability_registry.get_browser_capabilities(view_key): - slot = capability.slot # type: ignore[assignment] - action = self.action_registry.resolve_browser_action( - view_key=view_key, - slot=slot, - record=record, - env=env, - ) - if action is None: - continue - lines.append(TUI_COPY.build_action_line(capability.key, action.label)) - return lines or [TUI_COPY.build_action_empty_line()] - - @staticmethod - def build_action_result_message(result: TuiActionResult) -> str: - """ - 构建动作执行结果通知文本。 - - :param result: 动作执行结果 - :return: 通知文本 - """ - outcome = ( - TUI_COPY.build_action_result_field_label('success') - if result.ok - else TUI_COPY.build_action_result_field_label('fail') - ) - return TUI_COPY.build_action_result_toast( - result.spec.label, - outcome, - SHELL_TEXT_FORMATTER.truncate_text(result.message, 72), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/registry.py b/shierfeng-fastapi-backend/build/lib/cli/tui/actions/registry.py deleted file mode 100644 index 90534e2..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/actions/registry.py +++ /dev/null @@ -1,103 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from cli.tui.actions.builders import TuiActionSpecFactory, TuiActionTemplate - from cli.tui.actions.models import ActionSlot, TuiActionSpec - from cli.tui.adapters.models import BrowserRecordSnapshot - - -@dataclass(frozen=True) -class TuiActionRegistry: - """ - TUI 动作注册表。 - - 该注册表负责集中维护浏览页和详情页的动作解析器,从而将 - 原本散落的条件分发收敛为可声明的映射关系。 - - :param browser_resolvers: 浏览页动作槽位解析器映射 - :param detail_resolvers: 详情页动作槽位解析器映射 - """ - - browser_resolvers: dict[str, TuiActionSlotResolver] - detail_resolvers: dict[str, TuiActionSlotResolver] - - def resolve_browser_action( - self, - *, - view_key: str, - slot: ActionSlot, - record: BrowserRecordSnapshot | None, - env: str, - ) -> TuiActionSpec | None: - """ - 解析浏览页动作。 - - :param view_key: 当前页面视图标识 - :param slot: 动作槽位 - :param record: 当前选中记录 - :param env: 当前运行环境 - :return: 动作定义 - """ - resolver = self.browser_resolvers.get(view_key) - if resolver is None: - return None - return resolver.resolve(slot=slot, record=record, env=env) - - def resolve_detail_action( - self, - *, - view_key: str, - slot: ActionSlot, - env: str, - ) -> TuiActionSpec | None: - """ - 解析详情页动作。 - - :param view_key: 当前页面视图标识 - :param slot: 动作槽位 - :param env: 当前运行环境 - :return: 动作定义 - """ - resolver = self.detail_resolvers.get(view_key) - if resolver is None: - return None - return resolver.resolve(slot=slot, record=None, env=env) - - -@dataclass(frozen=True) -class TuiActionSlotResolver: - """ - TUI 动作槽位解析器。 - - 该对象将一个页面支持的槽位动作集中定义为映射关系,避免继续在 - `resolve_*_action` 中堆叠条件分支。 - - :param slot_templates: 槽位到动作模板的映射 - :param spec_factory: 动作规格构建器 - """ - - slot_templates: dict[ActionSlot, TuiActionTemplate] - spec_factory: TuiActionSpecFactory - - def resolve( - self, - *, - slot: ActionSlot, - record: BrowserRecordSnapshot | None, - env: str, - ) -> TuiActionSpec | None: - """ - 解析指定槽位的动作定义。 - - :param slot: 动作槽位 - :param record: 当前选中记录 - :param env: 当前运行环境 - :return: 动作定义 - """ - template = self.slot_templates.get(slot) - if template is None: - return None - return template.build(record=record, env=env, spec_factory=self.spec_factory) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/__init__.py deleted file mode 100644 index 55d1882..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass - -from cli.tui.adapters.app import APP_DETAIL_ADAPTER as _APP_DETAIL_ADAPTER -from cli.tui.adapters.cache import CACHE_BROWSER_ADAPTER as _CACHE_BROWSER_ADAPTER -from cli.tui.adapters.configs import CONFIGS_BROWSER_ADAPTER as _CONFIGS_BROWSER_ADAPTER -from cli.tui.adapters.crypto import CRYPTO_DETAIL_ADAPTER as _CRYPTO_DETAIL_ADAPTER -from cli.tui.adapters.database import DATABASE_DETAIL_ADAPTER as _DATABASE_DETAIL_ADAPTER -from cli.tui.adapters.gen import GEN_BROWSER_ADAPTER as _GEN_BROWSER_ADAPTER -from cli.tui.adapters.health import ( - DASHBOARD_ADAPTER as _DASHBOARD_ADAPTER, -) -from cli.tui.adapters.health import DashboardPanelSnapshot, DashboardSnapshot -from cli.tui.adapters.jobs import JOBS_BROWSER_ADAPTER as _JOBS_BROWSER_ADAPTER -from cli.tui.adapters.models import ( - BrowserPageSnapshot, - BrowserRecordSnapshot, - DetailPageSnapshot, - DetailSectionSnapshot, -) -from cli.tui.adapters.ops import OPS_DETAIL_ADAPTER as _OPS_DETAIL_ADAPTER - -PageSnapshot = BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot -SnapshotCollector = Callable[..., PageSnapshot] - - -@dataclass(frozen=True) -class TuiSnapshotCollectorRegistry: - """ - TUI 页面快照采集注册表。 - - 该对象集中维护页面视图标识与其采集函数之间的映射, - 使应用层和测试层可以通过统一入口发现当前可用的采集器。 - - :param collectors: 视图到采集函数的映射 - """ - - collectors: dict[str, SnapshotCollector] - - @staticmethod - def normalize_view_key(view_key: str) -> str: - """ - 规范化页面视图标识。 - - :param view_key: 原始页面视图标识 - :return: 规范化后的视图标识 - """ - return str(view_key).strip().lower() - - def get_collector(self, view_key: str) -> SnapshotCollector | None: - """ - 读取指定视图的快照采集器。 - - :param view_key: 页面视图标识 - :return: 采集函数 - """ - return self.collectors.get(self.normalize_view_key(view_key)) - - def collect(self, view_key: str, *args, **kwargs) -> PageSnapshot: - """ - 调用指定视图的快照采集器。 - - :param view_key: 页面视图标识 - :param args: 位置参数 - :param kwargs: 关键字参数 - :return: 页面快照 - :raises KeyError: 当视图未注册时抛出 - """ - collector = self.get_collector(view_key) - if collector is None: - raise KeyError(self.normalize_view_key(view_key)) - return collector(*args, **kwargs) - - -TUI_SNAPSHOT_COLLECTOR_REGISTRY = TuiSnapshotCollectorRegistry( - collectors={ - 'dashboard': _DASHBOARD_ADAPTER.collect_snapshot, - 'app': _APP_DETAIL_ADAPTER.collect_snapshot, - 'ops': _OPS_DETAIL_ADAPTER.collect_snapshot, - 'database': _DATABASE_DETAIL_ADAPTER.collect_snapshot, - 'cache': _CACHE_BROWSER_ADAPTER.collect_snapshot, - 'jobs': _JOBS_BROWSER_ADAPTER.collect_snapshot, - 'gen': _GEN_BROWSER_ADAPTER.collect_snapshot, - 'configs': _CONFIGS_BROWSER_ADAPTER.collect_snapshot, - 'crypto': _CRYPTO_DETAIL_ADAPTER.collect_snapshot, - } -) - -__all__ = [ - 'TUI_SNAPSHOT_COLLECTOR_REGISTRY', - 'BrowserPageSnapshot', - 'BrowserRecordSnapshot', - 'DashboardPanelSnapshot', - 'DashboardSnapshot', - 'DetailPageSnapshot', - 'DetailSectionSnapshot', - 'TuiSnapshotCollectorRegistry', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/app.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/app.py deleted file mode 100644 index 217ca23..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/app.py +++ /dev/null @@ -1,680 +0,0 @@ -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 - -COMPLETION_PREVIEW_LINE_LIMIT = 12 - - -@dataclass(frozen=True) -class AppDetailSourcePayloads: - """ - 应用详情页原始数据源快照。 - - :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 completion_preview_result: `completion show` 命令结果 - :param preview_shell: 当前补全脚本预览 shell - """ - - 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 - completion_preview_result: Any - preview_shell: str - - -class AppDetailSnapshotCollector: - """ - 应用详情页数据采集器。 - - 该对象负责拉取应用详情页所需的多路 CLI 原始结果, - 让 `AppDetailAdapter` 保持详情页编排职责。 - - :param section_builder: 应用详情分区构建器 - """ - - def __init__(self, section_builder: 'AppSectionBuilder') -> None: - """ - 初始化应用详情页数据采集器。 - - :param section_builder: 应用详情分区构建器 - :return: None - """ - self.section_builder = section_builder - - def collect(self, env: str) -> AppDetailSourcePayloads: - """ - 采集应用详情页所需原始结果。 - - :param env: 当前运行环境 - :return: 应用详情页原始数据源快照 - """ - env_payload = NESTED_CLI_SUPPORT.run( - 'app', - 'env', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - config_payload = NESTED_CLI_SUPPORT.run( - 'app', - 'config', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - doctor_payload = NESTED_CLI_SUPPORT.run( - 'app', - 'doctor', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - routes_payload = NESTED_CLI_SUPPORT.run( - 'app', - 'routes', - f'--env={env}', - '--group-by=tag', - '--output=json', - parse_json=True, - ).payload - completion_payload = NESTED_CLI_SUPPORT.run( - 'completion', - 'doctor', - '--output=json', - parse_json=True, - ).payload - preview_shell = self.section_builder.resolve_completion_preview_shell(completion_payload) - completion_preview_result = NESTED_CLI_SUPPORT.run('completion', 'show', preview_shell) - return AppDetailSourcePayloads( - env_payload=env_payload, - config_payload=config_payload, - doctor_payload=doctor_payload, - routes_payload=routes_payload, - completion_payload=completion_payload, - completion_preview_result=completion_preview_result, - preview_shell=preview_shell, - ) - - -class AppSectionBuilder: - """ - 应用详情分区构建器。 - - 该构建器负责将应用、补全和路由相关 CLI 结果转换为 TUI 详情页分区, - 使详情页适配器本体只保留采集与编排职责。 - """ - - @staticmethod - def resolve_completion_preview_shell(payload: dict[str, Any] | None) -> str: - """ - 解析补全脚本预览应使用的 shell。 - - :param payload: `completion doctor` JSON 负载 - :return: shell 名称 - """ - if not isinstance(payload, dict): - return 'bash' - active_shell = str(payload.get('activeShell') or '').strip().lower() - shells = payload.get('shells') if isinstance(payload.get('shells'), dict) else {} - if active_shell and isinstance(shells.get(active_shell), dict) and shells[active_shell].get('supported', False): - return active_shell - for shell_name in ('bash', 'zsh', 'fish', 'powershell'): - shell_payload = shells.get(shell_name) - if isinstance(shell_payload, dict) and shell_payload.get('supported', False): - return shell_name - return 'bash' - - @staticmethod - def build_env_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建应用环境解析分区。 - - :param payload: `app env` JSON 负载 - :return: 分区快照 - """ - runtime = payload.get('runtime') if isinstance(payload, dict) else None - if not isinstance(runtime, 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'CLI 目标环境: {runtime.get("cliEnv", "-")}', - f'配置模块环境: {runtime.get("configEnv", "-")}', - f'进程环境变量: {runtime.get("appEnv", "-") or "-"}', - '', - '## 文件与进程', - f'环境文件: {runtime.get("envFile", "-")}', - f'环境文件存在: {"是" if runtime.get("envFileExists", False) else "否"}', - f'后端目录: {SHELL_TEXT_FORMATTER.truncate_text(runtime.get("backendDir", "-"), 64)}', - f'Python 可执行文件: {SHELL_TEXT_FORMATTER.truncate_text(runtime.get("pythonExecutable", "-"), 64)}', - ], - ) - - @staticmethod - def build_config_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建应用基础配置分区。 - - :param payload: `app config` JSON 负载 - :return: 分区快照 - """ - config = payload.get('config') if isinstance(payload, dict) else None - if not isinstance(config, 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'应用名称: {SHELL_TEXT_FORMATTER.truncate_text(config.get("name", "-"), 40)}', - f'监听地址: {config.get("host", "-")}:{config.get("port", "-")}', - f'根路径: {config.get("rootPath", "-") or "/"}', - f'工作进程: {config.get("workers", "-")}', - f'热重载: {"开启" if config.get("reload", False) else "关闭"}', - '', - '## 文档与日志', - f'Swagger: {"关闭" if config.get("disableSwagger", False) else "开启"}', - f'ReDoc: {"关闭" if config.get("disableRedoc", False) else "开启"}', - f'日志级别: {config.get("logLevel", "-")}', - ], - ) - - @staticmethod - def build_dependency_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建应用依赖配置分区。 - - :param payload: `app config` JSON 负载 - :return: 分区快照 - """ - config = payload.get('config') if isinstance(payload, dict) else None - if not isinstance(config, 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'类型: {config.get("dbType", "-")}', - f'地址: {config.get("dbHost", "-")}:{config.get("dbPort", "-")}', - f'数据库名: {config.get("dbDatabase", "-")}', - '', - '## Redis 与加密', - f'Redis 地址: {config.get("redisHost", "-")}:{config.get("redisPort", "-")}', - f'传输加密: {"开启" if config.get("transportCryptoEnabled", False) else "关闭"}', - f'加密模式: {config.get("transportCryptoMode", "-")}', - ], - ) - - def build_routes_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建应用路由摘要分区。 - - :param payload: `app routes` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='路由摘要', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='路由', empty_value='不可用'), - ) - - count = payload.get('count', 0) - routes = payload.get('routes') if isinstance(payload.get('routes'), list) else [] - grouped_routes = payload.get('groupedRoutes') if isinstance(payload.get('groupedRoutes'), dict) else {} - group_items = sorted( - ((str(tag), len(items) if isinstance(items, list) else 0) for tag, items in grouped_routes.items()), - key=lambda item: (-item[1], item[0]), - ) - - lines = [ - '## 路由规模', - f'总路由数: {count}', - f'标签分组数: {len(group_items)}', - '', - '## 主要标签', - ] - if group_items: - lines.extend(f'> {tag} · {size} 条' for tag, size in group_items[:6]) - else: - lines.append('> 暂无标签分组数据') - - if routes: - lines.extend(['', '## 路由样本']) - for route in routes[:6]: - if not isinstance(route, dict): - continue - methods = '/'.join(route.get('methods', [])) if isinstance(route.get('methods'), list) else '-' - lines.extend( - [ - f'> [{methods or "-"}] {SHELL_TEXT_FORMATTER.truncate_text(route.get("path", "-"), 48)}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(route.get("summary", "-") or "-", 48)}', - ] - ) - else: - lines.extend( - [ - '', - *TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='路由样本', - empty_value='0 条', - detail='当前环境未返回可展示的路由样本', - suggestion=TUI_COPY.build_refresh_page_suggestion('应用', '确认当前环境是否已完成路由注册'), - ), - ] - ) - - return DetailSectionSnapshot( - title='路由摘要', - status='ok', - lines=lines, - ) - - @staticmethod - def build_completion_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建 shell completion 诊断分区。 - - :param payload: `completion doctor` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='补全诊断', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='补全诊断', empty_value='不可用' - ), - ) - - shells = payload.get('shells') if isinstance(payload.get('shells'), dict) else {} - active_shell = str(payload.get('activeShell') or '-') - env_choices = payload.get('envChoices') if isinstance(payload.get('envChoices'), list) else [] - active_shell_payload = shells.get(active_shell) if isinstance(shells.get(active_shell), dict) else None - target_file = active_shell_payload.get('targetFile', '-') if isinstance(active_shell_payload, dict) else '-' - source_command = ( - active_shell_payload.get('sourceCommand', '-') if isinstance(active_shell_payload, dict) else '-' - ) - install_command = ( - active_shell_payload.get('recommendedInstallCommand', '-') - if isinstance(active_shell_payload, dict) - else payload.get('recommendedInstallCommand', '-') - ) - - lines = [ - '## 当前环境', - f'活动 Shell: {active_shell}', - f'项目目录: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("projectDir", "-"), 64)}', - f'补全环境变量: {payload.get("completeEnvVar", "-")}', - f'环境候选数: {len(env_choices)}', - '', - '## 激活建议', - f'目标脚本: {SHELL_TEXT_FORMATTER.truncate_text(target_file, 64)}', - f'加载命令: {SHELL_TEXT_FORMATTER.truncate_text(source_command, 72)}', - f'推荐安装命令: {SHELL_TEXT_FORMATTER.truncate_text(install_command, 72)}', - ] - - shell_lines = [] - for shell_name in ('bash', 'zsh', 'fish', 'powershell'): - shell_payload = shells.get(shell_name) - if not isinstance(shell_payload, dict): - continue - supported = '支持' if shell_payload.get('supported', False) else '不支持' - detected = '当前 Shell' if shell_payload.get('detected', False) else '候选 Shell' - shell_lines.append(f'> {shell_name} · {supported} · {detected}') - if shell_lines: - lines.extend(['', '## Shell 支持']) - lines.extend(shell_lines) - - return DetailSectionSnapshot( - title='补全诊断', - status='ok', - lines=lines, - ) - - def build_completion_preview_section( - self, - shell: str, - preview_result: Any, - ) -> DetailSectionSnapshot: - """ - 构建补全脚本预览分区。 - - :param shell: 当前预览 shell - :param preview_result: `completion show` 调用结果 - :return: 分区快照 - """ - stdout = str(getattr(preview_result, 'stdout', '') or '').strip() - stderr = str(getattr(preview_result, 'stderr', '') or '').strip() - returncode = int(getattr(preview_result, 'returncode', 1) or 0) - if returncode != 0: - return DetailSectionSnapshot( - title='补全脚本预览', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - {'ok': False, 'message': stderr or stdout or '补全脚本生成失败'}, - empty_label='补全脚本', - empty_value='不可用', - ), - ) - preview_lines = [ - SHELL_TEXT_FORMATTER.truncate_text(line, 88) for line in stdout.splitlines()[:COMPLETION_PREVIEW_LINE_LIMIT] - ] - if not preview_lines: - return DetailSectionSnapshot( - title='补全脚本预览', - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='补全脚本', - empty_value='0 行', - detail='当前 shell 的补全脚本未返回可预览内容', - suggestion=TUI_COPY.build_refresh_page_suggestion('应用', '重新生成补全脚本预览'), - ), - ) - return DetailSectionSnapshot( - title='补全脚本预览', - status='ok', - lines=[ - '## 预览目标', - f'Shell: {shell}', - f'预览行数: {len(preview_lines)}', - '', - '## 脚本片段', - *preview_lines, - ], - ) - - @staticmethod - def build_doctor_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建应用启动前检查分区。 - - :param payload: `app doctor` 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='不可用' - ), - ) - - 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 DetailSectionSnapshot( - title='启动前检查', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 检查结果', - f'环境: {payload.get("env", "-")}', - f'数据库: {"正常" if database.get("ok", False) else "异常"}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(database.get("message", "-"), 64)}', - f'Redis: {"正常" if redis.get("ok", False) else "异常"}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(redis.get("message", "-"), 64)}', - f'加密组件: {"正常" if crypto.get("ok", False) else "异常"}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(crypto.get("message", "-"), 64)}', - ], - ) - - @staticmethod - def build_run_entry_section() -> DetailSectionSnapshot: - """ - 构建应用启动向导入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='启动入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备启动当前环境应用时,可直接启动应用,或先通过向导确认目标环境与启动前检查选项。', - command=TUI_COPY.build_cli_command_hint('app', 'run', '--env=dev'), - guide='若需要先做启动前检查或切换环境,建议改用 `wizard app-run` 进入交互式启动流程。', - ), - ) - - @staticmethod - def build_completion_install_entry_section() -> DetailSectionSnapshot: - """ - 构建 shell 补全安装入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='补全安装入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备为当前 Shell 安装或修复补全脚本时,可直接执行补全安装命令,并按需把激活命令写入 rc 文件。', - command=TUI_COPY.build_cli_command_hint('completion', 'install', '--activate', '--output=text'), - guide='当前页面提供补全诊断与安装入口;执行安装后可返回补全诊断分区确认 target file 和 source command 是否更新。', - ), - ) - - @staticmethod - def build_overview_section( - 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, - ) -> DetailSectionSnapshot: - """ - 构建应用页总览判断分区。 - - :param env_payload: `app env` JSON 负载 - :param config_payload: `app config` JSON 负载 - :param doctor_payload: `app doctor` JSON 负载 - :param routes_payload: `app routes` JSON 负载 - :param completion_payload: `completion doctor` JSON 负载 - :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 - runtime = ( - env_payload.get('runtime') - if isinstance(env_payload, dict) and isinstance(env_payload.get('runtime'), dict) - else {} - ) - config = ( - config_payload.get('config') - if isinstance(config_payload, dict) and isinstance(config_payload.get('config'), dict) - else {} - ) - - status = 'ok' - conclusion = '应用基线正常,可继续查看环境映射、配置摘要与路由状态' - if not env_ok or not config_ok: - status = 'fail' - conclusion = '应用基础信息读取异常,优先确认环境映射与应用配置是否可用' - elif not doctor_ok: - status = 'fail' - conclusion = '启动前检查异常,优先确认数据库、Redis 和加密组件状态' - elif not completion_ok: - status = 'warn' - conclusion = '补全诊断读取异常,建议优先确认项目目录、Shell 类型和补全脚本状态' - elif not routes_ok: - status = 'warn' - conclusion = '路由摘要读取异常,建议先确认当前环境是否完成应用初始化' - elif int(route_count or 0) <= 0: - status = 'info' - conclusion = '当前未返回注册路由,建议检查路由注册流程与运行上下文' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'CLI 环境: {runtime.get("cliEnv", "-")}', - f'配置环境: {runtime.get("configEnv", "-")}', - f'监听地址: {config.get("host", "-")}:{config.get("port", "-")}', - f'注册路由: {route_count} 条', - f'启动前检查: {"正常" if doctor_ok else "异常"}', - f'传输加密: {"开启" if config.get("transportCryptoEnabled", False) else "关闭"}', - f'补全诊断: {"正常" if completion_ok else "异常"}', - '', - '## 建议入口', - '优先关注:环境解析 / 应用配置 / 启动前检查 / 补全诊断 / 路由摘要 / 启动入口', - ], - ) - - def build_sections( - 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, - completion_preview_result: Any, - preview_shell: str, - ) -> list[DetailSectionSnapshot]: - """ - 构建应用页全部详情分区。 - - :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 completion_preview_result: `completion show` 命令结果 - :param preview_shell: 当前预览 shell - :return: 详情分区列表 - """ - return [ - self.build_overview_section( - env_payload, - config_payload, - doctor_payload, - routes_payload, - completion_payload, - ), - self.build_env_section(env_payload), - self.build_config_section(config_payload), - self.build_dependency_section(config_payload), - self.build_doctor_section(doctor_payload), - self.build_completion_section(completion_payload), - self.build_completion_preview_section(preview_shell, completion_preview_result), - self.build_completion_install_entry_section(), - self.build_routes_section(routes_payload), - self.build_run_entry_section(), - ] - - -class AppDetailAdapter(BaseDetailAdapter): - """ - 应用详情页适配器。 - - 该适配器负责采集应用相关 CLI 结果,并委托分区构建器组装为 - TUI 详情页快照。 - """ - - def __init__( - self, - section_builder: AppSectionBuilder | None = None, - snapshot_collector: AppDetailSnapshotCollector | None = None, - ) -> None: - """ - 初始化应用详情页适配器。 - - :param section_builder: 应用详情分区构建器 - :param snapshot_collector: 应用详情页数据采集器 - :return: None - """ - super().__init__( - page_title='应用', - search_view_key='app', - default_suggestions=[ - '总览判断', - '环境解析', - '应用配置', - '依赖配置', - '启动前检查', - '补全诊断', - '补全脚本预览', - '补全安装入口', - '路由摘要', - '启动入口', - ], - ) - self.section_builder = section_builder or AppSectionBuilder() - self.snapshot_collector = snapshot_collector or AppDetailSnapshotCollector(self.section_builder) - - 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( - env_payload=source_payloads.env_payload, - config_payload=source_payloads.config_payload, - doctor_payload=source_payloads.doctor_payload, - routes_payload=source_payloads.routes_payload, - completion_payload=source_payloads.completion_payload, - completion_preview_result=source_payloads.completion_preview_result, - preview_shell=source_payloads.preview_shell, - ) - return DetailPageSnapshot( - title='应用', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_app_diagnostic_subtitle( - source_payloads.env_payload, - source_payloads.config_payload, - source_payloads.doctor_payload, - source_payloads.routes_payload, - source_payloads.completion_payload, - ), - sections=self.filter_sections(sections, query), - search=self.resolve_search_context(query), - ) - - -APP_DETAIL_ADAPTER = AppDetailAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/base.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/base.py deleted file mode 100644 index 5e3308d..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/base.py +++ /dev/null @@ -1,219 +0,0 @@ -from collections.abc import Sequence -from typing import Any - -from cli.tui.adapters.models import ( - TUI_ADAPTER_MODEL_RENDERER, - BrowserRecordSnapshot, - DetailSectionSnapshot, -) -from cli.tui.copy import TUI_COPY -from cli.tui.search import TUI_SEARCH_SERVICE, PageFilterOption, PageSearchContext -from cli.utils import SHELL_TEXT_FORMATTER - - -class BaseBrowserAdapter: - """ - TUI 浏览页适配基类。 - - :param page_title: 浏览页标题 - :param search_view_key: 搜索上下文对应的页面键 - :param filter_options: 页面支持的筛选项 - """ - - def __init__( - self, - *, - page_title: str, - search_view_key: str, - filter_options: Sequence[PageFilterOption], - ) -> None: - """ - 初始化浏览页适配基类。 - - :param page_title: 浏览页标题 - :param search_view_key: 搜索上下文对应的页面键 - :param filter_options: 页面支持的筛选项 - :return: None - """ - self.page_title = page_title - self.search_view_key = search_view_key - self.filter_options = tuple(filter_options) - - @staticmethod - def extract_page_rows(payload: dict[str, Any] | None) -> list[dict[str, Any]]: - """ - 从标准分页 JSON 负载中提取行数据。 - - :param payload: 标准分页结果负载 - :return: 行数据列表 - """ - page_payload = payload.get('page') if isinstance(payload, dict) else None - rows = page_payload.get('rows') if isinstance(page_payload, dict) else None - if not isinstance(rows, list): - return [] - return [row for row in rows if isinstance(row, dict)] - - def resolve_active_filter(self, filter_key: str) -> PageFilterOption: - """ - 解析当前激活筛选项。 - - :param filter_key: 筛选键 - :return: 已解析的筛选项 - """ - return TUI_SEARCH_SERVICE.resolve_filter_option(self.filter_options, filter_key) or self.filter_options[0] - - def resolve_search_context(self, query: str) -> PageSearchContext: - """ - 解析当前页面搜索上下文。 - - :param query: 搜索词 - :return: 搜索上下文 - """ - return TUI_SEARCH_SERVICE.resolve_search_context(self.search_view_key, query) - - def build_failure_record( - self, - *, - key: str, - subject: str, - section_subject: str, - payload: dict[str, Any] | None, - ) -> BrowserRecordSnapshot: - """ - 构建底层命令失败时的兜底记录。 - - :param key: 记录键 - :param subject: 页面主体名称 - :param section_subject: 失败分区名称 - :param payload: 失败结果负载 - :return: 浏览记录快照 - """ - return BrowserRecordSnapshot( - key=key, - title=TUI_COPY.build_unavailable_record_title(subject), - status='fail', - summary=SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(payload), 64), - metadata_lines=[], - detail_sections=[ - DetailSectionSnapshot( - title=TUI_COPY.build_load_failure_section_title(section_subject), - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label=section_subject, empty_value='不可用' - ), - ) - ], - ) - - def build_empty_record( - self, - *, - key: str, - subject: str, - empty_label: str, - has_source_rows: bool, - filtered_summary: str, - empty_summary: str, - filtered_empty_value: str, - empty_empty_value: str, - filtered_detail: str, - empty_detail: str, - ) -> BrowserRecordSnapshot: - """ - 构建当前页面没有可浏览记录时的空态记录。 - - :param key: 记录键 - :param subject: 记录主体名称 - :param empty_label: 空态主字段名称 - :param has_source_rows: 是否存在未筛选前的源数据 - :param filtered_summary: 有源数据但当前筛选无结果时的摘要 - :param empty_summary: 源数据本身为空时的摘要 - :param filtered_empty_value: 有源数据但当前筛选无结果时的空态值 - :param empty_empty_value: 源数据本身为空时的空态值 - :param filtered_detail: 有源数据但当前筛选无结果时的说明 - :param empty_detail: 源数据本身为空时的说明 - :return: 浏览记录快照 - """ - summary = filtered_summary if has_source_rows else empty_summary - empty_value = filtered_empty_value if has_source_rows else empty_empty_value - detail = filtered_detail if has_source_rows else empty_detail - return BrowserRecordSnapshot( - key=key, - title=TUI_COPY.build_empty_record_title(subject), - status='info', - summary=TUI_COPY.build_empty_record_summary(summary), - metadata_lines=[], - detail_sections=[ - DetailSectionSnapshot( - title=TUI_COPY.build_empty_record_title(subject), - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label=empty_label, - empty_value=empty_value, - detail=detail, - ), - ) - ], - ) - - @staticmethod - def extract_payload_message(payload: dict[str, object] | None) -> str: - """ - 提取结果负载中的可读消息。 - - :param payload: 标准结果负载 - :return: 摘要消息文本 - """ - return TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(payload) - - -class BaseDetailAdapter: - """ - TUI 详情页适配基类。 - - :param page_title: 详情页标题 - :param search_view_key: 搜索上下文对应的页面键 - :param default_suggestions: 默认搜索建议 - """ - - def __init__( - self, - *, - page_title: str, - search_view_key: str, - default_suggestions: Sequence[str], - ) -> None: - """ - 初始化详情页适配基类。 - - :param page_title: 详情页标题 - :param search_view_key: 搜索上下文对应的页面键 - :param default_suggestions: 默认搜索建议 - :return: None - """ - self.page_title = page_title - self.search_view_key = search_view_key - self.default_suggestions = tuple(default_suggestions) - - def filter_sections(self, sections: Sequence[DetailSectionSnapshot], query: str) -> list[DetailSectionSnapshot]: - """ - 按搜索词过滤详情分区列表。 - - :param sections: 原始详情分区列表 - :param query: 搜索词 - :return: 过滤后的分区列表 - """ - return TUI_SEARCH_SERVICE.filter_detail_sections(list(sections), query) # type: ignore[arg-type] - - def resolve_search_context(self, query: str) -> PageSearchContext: - """ - 解析当前详情页搜索上下文。 - - :param query: 搜索词 - :return: 搜索上下文 - """ - return TUI_SEARCH_SERVICE.resolve_search_context( - self.search_view_key, - query, - default_suggestions=list(self.default_suggestions), - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/cache.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/cache.py deleted file mode 100644 index a3596be..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/cache.py +++ /dev/null @@ -1,579 +0,0 @@ -from typing import Any - -from cli.tui.adapters.base import BaseBrowserAdapter -from cli.tui.adapters.models import ( - TUI_ADAPTER_MODEL_RENDERER, - BrowserPageSnapshot, - BrowserRecordSnapshot, - 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 - - -class CacheRowExtractor: - """ - 缓存浏览行提取器。 - - 该对象负责从缓存统计结果中提取缓存名称列表,供共享分区和记录构建 - 逻辑复用。 - """ - - @staticmethod - def extract_cache_name_rows(payload: dict[str, Any] | None) -> list[dict[str, Any]]: - """ - 从缓存统计结果中提取缓存名称列表。 - - :param payload: `cache stats` JSON 负载 - :return: 缓存名称行列表 - """ - if not isinstance(payload, dict): - return [] - cache_names = payload.get('cacheNames') - if not isinstance(cache_names, list): - return [] - rows: list[dict[str, Any]] = [] - for item in cache_names: - if isinstance(item, dict): - rows.append(item) - continue - if isinstance(item, str): - rows.append({'cacheName': item, 'remark': ''}) - return rows - - -class CacheSectionBuilder: - """ - 缓存浏览页分区构建器。 - - 该构建器负责构建缓存浏览页共享分区,以及单个缓存名前缀下的键摘要、 - 键列表和键详情分区。 - - :param page_adapter: 缓存浏览页适配器 - :param row_extractor: 缓存浏览行提取器 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - row_extractor: CacheRowExtractor | None = None, - ) -> None: - """ - 初始化缓存浏览页分区构建器。 - - :param page_adapter: 缓存浏览页适配器 - :param row_extractor: 缓存浏览行提取器 - :return: None - """ - self.page_adapter = page_adapter - self.row_extractor = row_extractor or CacheRowExtractor() - - def build_overview_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建缓存总览共享分区。 - - :param payload: `cache stats` JSON 负载 - :return: 分区快照 - """ - info = payload.get('info') if isinstance(payload, dict) and isinstance(payload.get('info'), dict) else {} - cache_rows = self.row_extractor.extract_cache_name_rows(payload) - return DetailSectionSnapshot( - title='Redis 摘要', - status='ok' if isinstance(payload, dict) and payload.get('ok', False) else 'fail', - lines=[ - '## 容量状态', - f'当前键数: {payload.get("dbSize", "-") if isinstance(payload, dict) else "-"}', - f'已登记缓存名: {len(cache_rows)} 个', - '', - '## 运行指标', - f'Redis 版本: {info.get("redis_version", "-")}', - f'客户端连接数: {info.get("connected_clients", "-")}', - f'内存占用: {info.get("used_memory_human", info.get("used_memory", "-"))}', - ], - ) - - def build_overview_judgement_section( - self, - payload: dict[str, Any] | None, - filtered_rows: list[dict[str, Any]], - ) -> DetailSectionSnapshot: - """ - 构建缓存页总览判断共享分区。 - - :param payload: `cache stats` JSON 负载 - :param filtered_rows: 当前筛选后的缓存名列表 - :return: 分区快照 - """ - info = payload.get('info') if isinstance(payload, dict) and isinstance(payload.get('info'), dict) else {} - cache_rows = self.row_extractor.extract_cache_name_rows(payload) - db_size = payload.get('dbSize', '-') if isinstance(payload, dict) else '-' - client_count = info.get('connected_clients', '-') - command_stats = payload.get('commandStats') if isinstance(payload, dict) else None - command_count = len(command_stats) if isinstance(command_stats, list) else 0 - - status = 'ok' - conclusion = '缓存基线正常,可继续查看键列表、键值样本与 TTL' - if not isinstance(payload, dict) or not payload.get('ok', False): - status = 'fail' - conclusion = '缓存状态读取失败,优先确认 Redis 连通性与运行环境' - elif not cache_rows: - status = 'info' - conclusion = '当前没有登记的缓存名前缀,可先核对缓存配置与实际键空间' - elif not filtered_rows: - status = 'info' - conclusion = '当前搜索条件没有命中缓存名前缀,可调整关键字后继续排查' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'已登记缓存名: {len(cache_rows)} 个', - f'当前匹配: {len(filtered_rows)} 个', - f'Redis 键数: {db_size}', - f'客户端连接数: {client_count}', - f'命令统计样本: {command_count} 组', - '', - '## 建议入口', - '优先关注:Redis 摘要 / 命令统计 / 键值样本 / TTL', - ], - ) - - def build_top_commands_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建缓存命令统计共享分区。 - - :param payload: `cache stats` JSON 负载 - :return: 分区快照 - """ - command_stats = payload.get('commandStats') if isinstance(payload, dict) else None - lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='命令统计', - empty_value='暂无数据', - detail='当前 Redis 运行快照中未返回命令统计样本', - ) - section_status = 'info' - if isinstance(command_stats, list) and command_stats: - section_status = 'ok' - lines = [ - f'{item.get("name", "-")} · {item.get("value", 0)} 次' - for item in command_stats[:8] - if isinstance(item, dict) - ] - return DetailSectionSnapshot( - title='命令统计', - status=section_status if isinstance(payload, dict) and payload.get('ok', False) else 'fail', - lines=lines, - ) - - @staticmethod - def build_cache_clear_entry_section() -> DetailSectionSnapshot: - """ - 构建缓存清理向导入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='缓存清理入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='当缓存命中异常、键数量失控,或需要按缓存名和前缀清理时,应先通过向导确认影响范围。', - command=TUI_COPY.build_cli_command_hint('wizard', 'cache-clear', '--output=text'), - guide='向导会继续询问环境、缓存名、键前缀和 dry-run 选项,确认后再执行实际清理。', - ), - ) - - def build_keys_summary_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建缓存键摘要分区。 - - :param payload: `cache keys` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='键摘要', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='键摘要', empty_value='不可用' - ), - ) - keys = payload.get('keys') if isinstance(payload.get('keys'), list) else [] - rendered_keys = [str(item).strip() for item in keys if str(item).strip()] - lines = [ - '## 统计', - f'缓存名: {payload.get("cacheName", "-")}', - f'键数量: {payload.get("count", len(rendered_keys))}', - ] - if rendered_keys: - lines.extend( - ['', '## 键样本', *[f'> {SHELL_TEXT_FORMATTER.truncate_text(item, 64)}' for item in rendered_keys[:5]]] - ) - else: - lines.extend(['', '## 键样本', '> 当前缓存名前缀下没有键']) - return DetailSectionSnapshot( - title='键摘要', - status='ok', - lines=lines, - ) - - def build_keys_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建缓存键列表分区。 - - :param payload: `cache keys` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='键列表', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='键列表', empty_value='不可用' - ), - ) - keys = payload.get('keys') if isinstance(payload.get('keys'), list) else [] - lines = [ - SHELL_TEXT_FORMATTER.truncate_text(item, 88) for item in keys[:12] if str(item).strip() - ] or TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='缓存键', - empty_value='0 个', - detail='当前缓存名前缀下没有可展示的缓存键', - ) - return DetailSectionSnapshot( - title='键列表', - status='ok', - lines=lines, - ) - - def render_ttl_text(self, payload: dict[str, Any] | None) -> str: - """ - 渲染缓存 TTL 文本。 - - :param payload: `cache ttl` JSON 负载 - :return: TTL 文本 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return SHELL_TEXT_FORMATTER.truncate_text(self.page_adapter.extract_payload_message(payload), 56) - ttl_seconds = payload.get('ttlSeconds', '-') - if payload.get('persistent', False): - return '永久' - if payload.get('expires', False): - return f'{ttl_seconds} 秒' - return str(ttl_seconds) - - def build_key_detail_sections( - self, - cache_name: str, - key_items: list[str], - env: str, - ) -> list[DetailSectionSnapshot]: - """ - 构建按缓存键展开的详情分区。 - - :param cache_name: 缓存名称 - :param key_items: 缓存键列表 - :param env: 当前运行环境 - :return: 分区快照列表 - """ - if not key_items: - return [] - - sections: list[DetailSectionSnapshot] = [] - for cache_key in key_items[:5]: - value_payload = NESTED_CLI_SUPPORT.run( - 'cache', - 'get', - cache_name, - cache_key, - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - ttl_payload = NESTED_CLI_SUPPORT.run( - 'cache', - 'ttl', - cache_name, - cache_key, - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - - value_ok = isinstance(value_payload, dict) and value_payload.get('ok', False) - ttl_ok = isinstance(ttl_payload, dict) and ttl_payload.get('ok', False) - if value_ok and ttl_ok: - section_status = 'ok' - elif value_ok or ttl_ok: - section_status = 'warn' - else: - section_status = 'fail' - - cache_value = ( - '' if not value_ok or value_payload.get('cacheValue') is None else str(value_payload.get('cacheValue')) - ) - preview_lines = cache_value.splitlines()[:6] - ttl_text = self.render_ttl_text(ttl_payload) - lines = [ - '## 键信息', - f'缓存名: {cache_name}', - f'缓存键: {SHELL_TEXT_FORMATTER.truncate_text(cache_key, 72)}', - f'TTL: {ttl_text}', - '', - '## 值预览', - ] - if value_ok: - lines.extend( - [f'> {SHELL_TEXT_FORMATTER.truncate_text(line, 72)}' for line in preview_lines] - if preview_lines - else ['> -'] - ) - else: - lines.append( - f'> {SHELL_TEXT_FORMATTER.truncate_text(self.page_adapter.extract_payload_message(value_payload), 72)}' - ) - if not ttl_ok: - lines.extend( - [ - '', - '## TTL 结果', - f'> {SHELL_TEXT_FORMATTER.truncate_text(self.page_adapter.extract_payload_message(ttl_payload), 72)}', - ] - ) - sections.append( - DetailSectionSnapshot( - title=f'键详情 · {SHELL_TEXT_FORMATTER.truncate_text(cache_key, 28)}', - status=section_status, - lines=lines, - ) - ) - return sections - - def load_cache_detail_sections( - self, - cache_row: dict[str, Any], - env: str, - ) -> list[DetailSectionSnapshot]: - """ - 按需加载单个缓存名称的详情分区。 - - :param cache_row: 缓存名称行数据 - :param env: 当前运行环境 - :return: 详情分区列表 - """ - cache_name = str(cache_row.get('cacheName', '-') or '-') - keys_payload = NESTED_CLI_SUPPORT.run( - 'cache', - 'keys', - cache_name, - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - key_items: list[str] = [] - if isinstance(keys_payload, dict) and keys_payload.get('ok', False): - raw_keys = keys_payload.get('keys') if isinstance(keys_payload.get('keys'), list) else [] - key_items = [str(item).strip() for item in raw_keys if str(item).strip()] - return [ - self.build_keys_summary_section(keys_payload), - self.build_keys_section(keys_payload), - *self.build_key_detail_sections(cache_name, key_items, env), - ] - - def build_shared_sections( - self, - payload: dict[str, Any] | None, - filtered_rows: list[dict[str, Any]], - ) -> list[DetailSectionSnapshot]: - """ - 构建缓存浏览页共享分区。 - - :param payload: `cache stats` 结果负载 - :param filtered_rows: 当前筛选后的缓存名列表 - :return: 共享分区列表 - """ - return [ - self.build_overview_judgement_section(payload, filtered_rows), - self.build_overview_section(payload), - self.build_top_commands_section(payload), - self.build_cache_clear_entry_section(), - ] - - -class CacheRecordBuilder: - """ - 缓存浏览记录构建器。 - - 该对象负责构建缓存页单条浏览记录与失败兜底记录。 - - :param page_adapter: 缓存浏览页适配器 - :param section_builder: 缓存浏览页分区构建器 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - section_builder: CacheSectionBuilder, - ) -> None: - """ - 初始化缓存浏览记录构建器。 - - :param page_adapter: 缓存浏览页适配器 - :param section_builder: 缓存浏览页分区构建器 - :return: None - """ - self.page_adapter = page_adapter - self.section_builder = section_builder - - def build_record(self, cache_row: dict[str, Any], env: str) -> BrowserRecordSnapshot: - """ - 构建单条缓存浏览记录。 - - :param cache_row: 缓存名称行数据 - :param env: 当前运行环境 - :return: 浏览记录快照 - """ - cache_name = str(cache_row.get('cacheName', '-') or '-') - remark = str(cache_row.get('remark', '') or '').strip() - return BrowserRecordSnapshot( - key=f'cache:{cache_name}', - title=SHELL_TEXT_FORMATTER.truncate_text(cache_name, 40), - status='ok', - summary=SHELL_TEXT_FORMATTER.truncate_text(remark or '查看当前缓存名前缀下的键、值样本和过期时间', 64), - metadata_lines=[ - '## 缓存身份', - f'缓存名称: {cache_name}', - f'用途说明: {SHELL_TEXT_FORMATTER.truncate_text(remark or "-", 64)}', - ], - detail_sections=[], - detail_loader=lambda cache_row=cache_row, env=env: self.section_builder.load_cache_detail_sections( - cache_row, env - ), - ) - - def build_failure_record(self, payload: dict[str, Any] | None) -> BrowserRecordSnapshot: - """ - 构建缓存页失败兜底记录。 - - :param payload: 失败结果负载 - :return: 浏览记录快照 - """ - return self.page_adapter.build_failure_record( - key='cache:unavailable', - subject='缓存', - section_subject='缓存状态', - payload=payload, - ) - - -class CacheBrowserAdapter(BaseBrowserAdapter): - """ - 缓存浏览页适配器。 - - 该适配器负责采集 Redis 统计和缓存名前缀列表,并委托协作对象构建 - 共享分区、单条记录与键详情分区。 - """ - - def __init__( - self, - row_extractor: CacheRowExtractor | None = None, - section_builder: CacheSectionBuilder | None = None, - record_builder: CacheRecordBuilder | None = None, - ) -> None: - """ - 初始化缓存浏览页适配器。 - - :param row_extractor: 缓存浏览行提取器 - :param section_builder: 缓存浏览页分区构建器 - :param record_builder: 缓存浏览记录构建器 - :return: None - """ - super().__init__( - page_title='缓存', - search_view_key='cache', - filter_options=(), - ) - self.row_extractor = row_extractor or CacheRowExtractor() - self.section_builder = section_builder or CacheSectionBuilder(self, self.row_extractor) - self.record_builder = record_builder or CacheRecordBuilder(self, self.section_builder) - - @staticmethod - def apply_cache_query(rows: list[dict[str, Any]], query: str) -> list[dict[str, Any]]: - """ - 按缓存名或备注查询词过滤缓存行数据。 - - :param rows: 原始缓存行列表 - :param query: 当前搜索词 - :return: 过滤后的缓存行列表 - """ - normalized_query = str(query).strip().lower() - if not normalized_query: - return rows - return [ - row - for row in rows - if normalized_query in str(row.get('cacheName', '') or '').strip().lower() - or normalized_query in str(row.get('remark', '') or '').strip().lower() - ] - - def collect_snapshot(self, env: str, query: str = '') -> BrowserPageSnapshot: - """ - 采集缓存浏览页只读快照。 - - :param env: 当前运行环境 - :param query: 当前搜索词 - :return: 浏览页快照 - """ - search_context = self.resolve_search_context(query) - payload = NESTED_CLI_SUPPORT.run( - 'cache', - 'stats', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - cache_rows = self.row_extractor.extract_cache_name_rows(payload) - filtered_rows = self.apply_cache_query(cache_rows, query) - shared_sections = self.section_builder.build_shared_sections(payload, filtered_rows) - if not isinstance(payload, dict) or not payload.get('ok', False): - return BrowserPageSnapshot( - title='缓存', - subtitle=TUI_COPY.build_unavailable_subtitle( - '缓存状态', - SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(payload), 72), - ), - records=[self.record_builder.build_failure_record(payload)], - shared_sections=shared_sections, - search=search_context, - ) - - records = [self.record_builder.build_record(cache_row, env) for cache_row in filtered_rows[:12]] - if not records: - records = [ - self.build_empty_record( - key='cache:none', - subject='缓存名', - empty_label='缓存名前缀', - has_source_rows=bool(cache_rows), - filtered_summary='当前搜索条件下没有匹配缓存名前缀', - empty_summary='当前运行环境没有登记可浏览的缓存名前缀', - filtered_empty_value='暂无匹配', - empty_empty_value='暂无登记', - filtered_detail='当前搜索条件下没有匹配缓存名前缀', - empty_detail='当前运行环境没有登记可浏览的缓存名前缀', - ) - ] - return BrowserPageSnapshot( - title='缓存', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_cache_diagnostic_subtitle(payload, len(filtered_rows)), - records=records, - shared_sections=shared_sections, - search=search_context, - ) - - -CACHE_BROWSER_ADAPTER = CacheBrowserAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/configs.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/configs.py deleted file mode 100644 index 63710da..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/configs.py +++ /dev/null @@ -1,760 +0,0 @@ -from typing import Any - -from cli.tui.adapters.base import BaseBrowserAdapter -from cli.tui.adapters.models import ( - TUI_ADAPTER_MODEL_RENDERER, - BrowserPageSnapshot, - BrowserRecordSnapshot, - DetailSectionSnapshot, -) -from cli.tui.copy import TUI_COPY -from cli.tui.diagnostics import TUI_DIAGNOSTIC_SERVICE -from cli.tui.search import CONFIG_FILTER_OPTIONS -from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER - -CONFIG_RISK_PRIORITY = { - 'mismatch': 0, - 'missing-cache': 1, - 'orphan-cache': 2, - 'normal': 3, -} - -CONFIG_RISK_LABELS = { - 'mismatch': '值不一致', - 'missing-cache': '缓存缺失', - 'orphan-cache': '缓存孤立', - 'normal': '正常', -} - -CONFIG_RISK_STATUSES = { - 'mismatch': 'fail', - 'missing-cache': 'warn', - 'orphan-cache': 'warn', - 'normal': 'ok', -} - - -class ConfigRiskSupport: - """ - 参数配置风险分类支持对象。 - - 该对象负责从巡检结果提取风险集合、解析单项风险分类,以及执行 - 风险排序和筛选逻辑,供浏览页分区和记录构建复用。 - """ - - @staticmethod - def extract_config_issue_keys(payload: dict[str, Any] | None, field_name: str) -> set[str]: - """ - 从配置巡检结果中提取指定风险字段对应的配置键集合。 - - :param payload: `config doctor` JSON 负载 - :param field_name: 风险字段名 - :return: 配置键集合 - """ - items = payload.get(field_name) if isinstance(payload, dict) else None - if not isinstance(items, list): - return set() - return {item.strip() for item in items if isinstance(item, str) and item.strip()} - - def build_config_risk_sets(self, payload: dict[str, Any] | None) -> dict[str, set[str]]: - """ - 构建配置风险分类集合。 - - :param payload: `config doctor` JSON 负载 - :return: 按风险分类聚合的配置键集合 - """ - return { - 'mismatch': self.extract_config_issue_keys(payload, 'mismatchKeys'), - 'missing-cache': self.extract_config_issue_keys(payload, 'missingInCache'), - 'orphan-cache': self.extract_config_issue_keys(payload, 'orphanInCache'), - } - - @staticmethod - def resolve_config_risk_key(config_key: str, risk_sets: dict[str, set[str]]) -> str: - """ - 解析单个配置项的风险分类。 - - :param config_key: 配置键 - :param risk_sets: 风险集合 - :return: 风险分类键 - """ - normalized_key = str(config_key).strip() - if normalized_key in risk_sets.get('mismatch', set()): - return 'mismatch' - if normalized_key in risk_sets.get('missing-cache', set()): - return 'missing-cache' - if normalized_key in risk_sets.get('orphan-cache', set()): - return 'orphan-cache' - return 'normal' - - def build_config_sort_key( - self, - config_row: dict[str, Any], - risk_sets: dict[str, set[str]], - ) -> tuple[int, str]: - """ - 构建配置列表排序键。 - - :param config_row: 配置项行数据 - :param risk_sets: 风险集合 - :return: 排序键 - """ - config_key = str(config_row.get('configKey', '') or '').strip() - risk_key = self.resolve_config_risk_key(config_key, risk_sets) - return (CONFIG_RISK_PRIORITY.get(risk_key, 99), config_key) - - def apply_config_filter( - self, - rows: list[dict[str, Any]], - risk_sets: dict[str, set[str]], - filter_key: str, - ) -> list[dict[str, Any]]: - """ - 按筛选键过滤配置行数据。 - - :param rows: 原始配置行列表 - :param risk_sets: 风险集合 - :param filter_key: 当前筛选键 - :return: 过滤后的配置行列表 - """ - normalized_filter = str(filter_key).strip().lower() - if normalized_filter == 'risky': - return [ - row - for row in rows - if self.resolve_config_risk_key(str(row.get('configKey', '') or ''), risk_sets) != 'normal' - ] - if normalized_filter == 'mismatch': - return [ - row - for row in rows - if self.resolve_config_risk_key(str(row.get('configKey', '') or ''), risk_sets) == 'mismatch' - ] - if normalized_filter == 'cache-drift': - return [ - row - for row in rows - if self.resolve_config_risk_key(str(row.get('configKey', '') or ''), risk_sets) - in {'missing-cache', 'orphan-cache'} - ] - return rows - - @staticmethod - def apply_config_query(rows: list[dict[str, Any]], query: str) -> list[dict[str, Any]]: - """ - 按配置键或配置名称查询词过滤配置行数据。 - - :param rows: 原始配置行列表 - :param query: 当前搜索词 - :return: 过滤后的配置行列表 - """ - normalized_query = str(query).strip().lower() - if not normalized_query: - return rows - return [ - row - for row in rows - if normalized_query in str(row.get('configKey', '') or '').strip().lower() - or normalized_query in str(row.get('configName', '') or '').strip().lower() - ] - - @staticmethod - def append_orphan_cache_rows(rows: list[dict[str, Any]], orphan_keys: set[str]) -> list[dict[str, Any]]: - """ - 将仅存在于缓存侧的孤立配置补充为可浏览记录。 - - :param rows: 原始数据库配置行 - :param orphan_keys: 缓存孤立配置键集合 - :return: 补齐后的配置行列表 - """ - merged_rows = list(rows) - existing_keys = {str(row.get('configKey', '') or '').strip() for row in rows} - merged_rows.extend( - { - 'configId': '-', - 'configKey': orphan_key, - 'configName': '缓存孤立配置', - 'configType': '-', - 'configValue': '缓存侧残留,建议排查来源', - } - for orphan_key in sorted(orphan_keys - existing_keys) - ) - return merged_rows - - -class ConfigSectionBuilder: - """ - 参数配置浏览页分区构建器。 - - 该构建器负责构建参数配置页共享分区,以及单个配置项详情分区。 - - :param page_adapter: 参数配置浏览页适配器 - :param risk_support: 参数配置风险分类支持对象 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - risk_support: ConfigRiskSupport, - ) -> None: - """ - 初始化参数配置浏览页分区构建器。 - - :param page_adapter: 参数配置浏览页适配器 - :param risk_support: 参数配置风险分类支持对象 - :return: None - """ - self.page_adapter = page_adapter - self.risk_support = risk_support - - def build_config_doctor_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建参数配置巡检共享分区。 - - :param payload: `config doctor` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='配置巡检', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='巡检结果', empty_value='不可用' - ), - ) - return DetailSectionSnapshot( - title='配置巡检', - status='ok', - lines=[ - '## 一致性状态', - f'数据库配置: {payload.get("databaseCount", "-")} 项', - f'缓存同步配置: {payload.get("cacheCount", "-")} 项', - f'缓存缺失: {payload.get("missingInCacheCount", "-")} 项', - f'缓存孤立: {payload.get("orphanInCacheCount", "-")} 项', - f'值不一致: {payload.get("mismatchCount", "-")} 项', - ], - ) - - def build_config_issue_samples_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建参数配置不一致样本共享分区。 - - :param payload: `config doctor` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='异常样本', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='样本', empty_value='不可用'), - ) - - lines: list[str] = [] - missing_in_cache = payload.get('missingInCache') - orphan_in_cache = payload.get('orphanInCache') - mismatch_keys = payload.get('mismatchKeys') - - if isinstance(missing_in_cache, list) and missing_in_cache: - lines.extend( - f'缓存缺失示例:{SHELL_TEXT_FORMATTER.truncate_text(item, 64)}' - for item in missing_in_cache[:5] - if isinstance(item, str) - ) - if isinstance(orphan_in_cache, list) and orphan_in_cache: - lines.extend( - f'缓存孤立示例:{SHELL_TEXT_FORMATTER.truncate_text(item, 64)}' - for item in orphan_in_cache[:5] - if isinstance(item, str) - ) - if isinstance(mismatch_keys, list) and mismatch_keys: - lines.extend( - f'值不一致示例:{SHELL_TEXT_FORMATTER.truncate_text(item, 64)}' - for item in mismatch_keys[:5] - if isinstance(item, str) - ) - - issue_count = ( - int(payload.get('missingInCacheCount', 0)) - + int(payload.get('orphanInCacheCount', 0)) - + int(payload.get('mismatchCount', 0)) - ) - return DetailSectionSnapshot( - title='异常样本', - status='fail' if issue_count > 0 else 'ok', - lines=lines - if lines - else TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='异常样本', - empty_value='0 条', - detail='当前未发现配置异常样本', - ), - ) - - def build_config_set_entry_section(self, rows: list[dict[str, Any]]) -> DetailSectionSnapshot: - """ - 构建配置变更入口共享分区。 - - :param rows: 当前配置行列表 - :return: 分区快照 - """ - sample_key = '' - sample_value = '' - for row in rows: - config_key = str(row.get('configKey', '') or '').strip() - config_value = str(row.get('configValue', '') or '').strip() - if config_key: - sample_key = config_key - sample_value = config_value or 'new-value' - break - if not sample_key: - sample_key = 'demo.config.key' - sample_value = 'new-value' - return DetailSectionSnapshot( - title='配置变更入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备修复高风险配置、回填缓存缺失项或校正数据库与缓存值不一致时,应先在终端中明确目标键和值,再执行配置变更命令。', - command=TUI_COPY.build_cli_command_hint('config', 'set', sample_key, sample_value, '--output=text'), - guide='建议先在当前页面核对配置键、来源和现值;变更完成后再回到工作台执行参数缓存刷新,确认巡检结果恢复正常。', - ), - ) - - def build_high_risk_config_section( - self, - rows: list[dict[str, Any]], - risk_sets: dict[str, set[str]], - ) -> DetailSectionSnapshot: - """ - 构建高风险配置共享分区。 - - :param rows: 配置行列表 - :param risk_sets: 风险集合 - :return: 分区快照 - """ - risky_rows = [ - row - for row in sorted( - rows, key=lambda config_row: self.risk_support.build_config_sort_key(config_row, risk_sets) - ) - if self.risk_support.resolve_config_risk_key(str(row.get('configKey', '') or ''), risk_sets) != 'normal' - ] - if not risky_rows: - return DetailSectionSnapshot( - title='高风险配置', - status='ok', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='高风险配置', - empty_value='0 项', - detail='当前巡检结果中没有需要优先处理的高风险配置', - ), - ) - lines: list[str] = [] - for row in risky_rows[:8]: - config_key = str(row.get('configKey', '-') or '-') - config_name = str(row.get('configName', '-') or '-') - risk_key = self.risk_support.resolve_config_risk_key(config_key, risk_sets) - lines.append( - f'[{CONFIG_RISK_LABELS.get(risk_key, "风险")}] ' - f'{SHELL_TEXT_FORMATTER.truncate_text(config_key, 40)} · {SHELL_TEXT_FORMATTER.truncate_text(config_name, 20)}' - ) - return DetailSectionSnapshot( - title='高风险配置', - status='warn' if risky_rows else 'ok', - lines=lines, - ) - - def build_configs_overview_section( - self, - rows: list[dict[str, Any]], - filtered_rows: list[dict[str, Any]], - risk_sets: dict[str, set[str]], - filter_label: str, - ) -> DetailSectionSnapshot: - """ - 构建参数配置页总览判断共享分区。 - - :param rows: 原始配置行列表 - :param filtered_rows: 当前筛选后的配置行列表 - :param risk_sets: 风险集合 - :param filter_label: 当前筛选标签 - :return: 分区快照 - """ - mismatch_count = len(risk_sets.get('mismatch', set())) - drift_count = len(risk_sets.get('missing-cache', set())) + len(risk_sets.get('orphan-cache', set())) - risky_count = mismatch_count + drift_count - status = 'ok' - conclusion = '当前配置一致性正常,可继续抽查单项详情与同步状态' - if mismatch_count > 0: - status = 'warn' - conclusion = '存在值不一致配置,优先核对数据库与缓存是否同步' - elif drift_count > 0: - status = 'warn' - conclusion = '存在缓存漂移配置,建议优先确认缺失项与孤立项来源' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'当前筛选: {filter_label}', - f'已加载配置: {len(rows)} 项', - f'当前匹配: {len(filtered_rows)} 项', - f'高风险配置: {risky_count} 项', - f'值不一致: {mismatch_count} 项', - f'缓存漂移: {drift_count} 项', - '', - '## 建议入口', - '优先关注:高风险配置 / 值不一致 / 缓存漂移', - ], - ) - - def build_config_consistency_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建单个配置的一致性分区。 - - :param payload: `config get` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='同步状态', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='同步状态', empty_value='不可用' - ), - ) - lines = [ - '## 当前配置', - f'键名: {payload.get("key", "-")}', - f'读取来源: {payload.get("source", "-")}', - ] - source = str(payload.get('source', '-') or '-').strip().lower() - section_status = 'ok' - if source == 'both': - lines.append(f'数据库与缓存一致: {"是" if payload.get("inSync", False) else "否"}') - if not payload.get('inSync', False): - section_status = 'fail' - elif source == 'database': - lines.append('缓存状态: 缺失') - section_status = 'warn' - elif source == 'cache': - lines.append('数据库状态: 缺失') - section_status = 'warn' - return DetailSectionSnapshot( - title='同步状态', - status=section_status, - lines=lines, - ) - - def build_config_source_section( - self, - title: str, - config_item: dict[str, Any] | None, - *, - missing_text: str, - ) -> DetailSectionSnapshot: - """ - 构建配置来源详情分区。 - - :param title: 分区标题 - :param config_item: 配置来源详情 - :param missing_text: 缺失时提示 - :return: 分区快照 - """ - if not isinstance(config_item, dict): - return DetailSectionSnapshot( - title=title, - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label=title, - empty_value='未找到', - detail=missing_text, - ), - ) - return DetailSectionSnapshot( - title=title, - status='ok', - lines=[ - '## 基础信息', - f'配置 ID: {config_item.get("configId", "-")}', - f'键名: {SHELL_TEXT_FORMATTER.truncate_text(config_item.get("configKey", "-"), 48)}', - f'名称: {SHELL_TEXT_FORMATTER.truncate_text(config_item.get("configName", "-"), 32)}', - f'键值: {SHELL_TEXT_FORMATTER.truncate_text(config_item.get("configValue", "-"), 72)}', - f'内置标记: {config_item.get("configType", "-")}', - '', - '## 备注', - SHELL_TEXT_FORMATTER.truncate_text(config_item.get('remark', '-') or '-', 88), - ], - ) - - def load_config_detail_sections( - self, - config_row: dict[str, Any], - env: str, - ) -> list[DetailSectionSnapshot]: - """ - 按需加载单个配置项详情分区。 - - :param config_row: 配置项行数据 - :param env: 当前运行环境 - :return: 详情分区列表 - """ - config_key = str(config_row.get('configKey', '-') or '-') - detail_payload = NESTED_CLI_SUPPORT.run( - 'config', - 'get', - config_key, - f'--env={env}', - '--source=both', - '--output=json', - parse_json=True, - ).payload - database_payload = detail_payload.get('database') if isinstance(detail_payload, dict) else None - cache_payload = detail_payload.get('cache') if isinstance(detail_payload, dict) else None - return [ - self.build_config_consistency_section(detail_payload), - self.build_config_source_section('数据库配置', database_payload, missing_text='数据库中未找到该配置项'), - self.build_config_source_section('缓存配置', cache_payload, missing_text='缓存中未找到该配置项'), - ] - - -class ConfigRecordBuilder: - """ - 参数配置浏览记录构建器。 - - 该构建器负责构建配置浏览记录与失败兜底记录。 - - :param page_adapter: 参数配置浏览页适配器 - :param section_builder: 参数配置浏览页分区构建器 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - section_builder: ConfigSectionBuilder, - ) -> None: - """ - 初始化参数配置浏览记录构建器。 - - :param page_adapter: 参数配置浏览页适配器 - :param section_builder: 参数配置浏览页分区构建器 - :return: None - """ - self.page_adapter = page_adapter - self.section_builder = section_builder - - def build_record( - self, - config_row: dict[str, Any], - env: str, - *, - risk_key: str, - ) -> BrowserRecordSnapshot: - """ - 构建单条参数配置浏览记录。 - - :param config_row: 配置项行数据 - :param env: 当前运行环境 - :param risk_key: 当前配置项风险键 - :return: 浏览记录快照 - """ - config_id = config_row.get('configId', '-') - config_key = str(config_row.get('configKey', '-') or '-') - config_name = str(config_row.get('configName', '-') or '-') - config_type = str(config_row.get('configType', '-') or '-') - config_value = str(config_row.get('configValue', '-') or '-') - risk_label = CONFIG_RISK_LABELS.get(risk_key, '正常') - return BrowserRecordSnapshot( - key=f'config:{config_key}', - title=SHELL_TEXT_FORMATTER.truncate_text(config_key, 44), - status=CONFIG_RISK_STATUSES.get(risk_key, 'ok'), - summary=f'{risk_label} · {SHELL_TEXT_FORMATTER.truncate_text(config_name, 18)} · 值 {SHELL_TEXT_FORMATTER.truncate_text(config_value, 24)}', - metadata_lines=[ - '## 配置身份', - f'配置 ID: {config_id}', - f'配置名称: {SHELL_TEXT_FORMATTER.truncate_text(config_name, 40)}', - f'系统内置: {config_type}', - f'风险分类: {risk_label}', - '', - '## 当前值', - SHELL_TEXT_FORMATTER.truncate_text(config_value, 88), - ], - detail_sections=[], - detail_loader=lambda config_row=config_row, env=env: self.section_builder.load_config_detail_sections( - config_row, - env, - ), - ) - - def build_failure_record(self, payload: dict[str, Any] | None) -> BrowserRecordSnapshot: - """ - 构建配置页失败兜底记录。 - - :param payload: 失败结果负载 - :return: 浏览记录快照 - """ - return self.page_adapter.build_failure_record( - key='config:unavailable', - subject='参数配置', - section_subject='配置列表', - payload=payload, - ) - - -class ConfigsBrowserAdapter(BaseBrowserAdapter): - """ - 参数配置浏览页适配器。 - - 该适配器负责采集配置巡检、配置列表和单项配置详情,并统一完成 - 风险分类、筛选排序、共享分区构建与浏览记录装配。 - - :param risk_support: 参数配置风险分类支持对象 - :param section_builder: 参数配置浏览页分区构建器 - :param record_builder: 参数配置浏览记录构建器 - """ - - def __init__( - self, - risk_support: ConfigRiskSupport | None = None, - section_builder: ConfigSectionBuilder | None = None, - record_builder: ConfigRecordBuilder | None = None, - ) -> None: - """ - 初始化参数配置浏览页适配器。 - - :param risk_support: 参数配置风险分类支持对象 - :param section_builder: 参数配置浏览页分区构建器 - :param record_builder: 参数配置浏览记录构建器 - :return: None - """ - super().__init__( - page_title='参数配置', - search_view_key='configs', - filter_options=CONFIG_FILTER_OPTIONS, - ) - self.risk_support = risk_support or ConfigRiskSupport() - self.section_builder = section_builder or ConfigSectionBuilder(self, self.risk_support) - self.record_builder = record_builder or ConfigRecordBuilder(self, self.section_builder) - - def collect_snapshot(self, env: str, filter_key: str = 'all', query: str = '') -> BrowserPageSnapshot: - """ - 采集参数配置浏览页只读快照。 - - :param env: 当前运行环境 - :param filter_key: 当前筛选键 - :param query: 当前搜索词 - :return: 浏览页快照 - """ - active_filter_option = self.resolve_active_filter(filter_key) - active_filter = active_filter_option.key - active_filter_label = active_filter_option.label - search_context = self.resolve_search_context(query) - doctor_payload = NESTED_CLI_SUPPORT.run( - 'config', - 'doctor', - f'--env={env}', - '--sample-limit=5', - '--output=json', - parse_json=True, - ).payload - list_payload = NESTED_CLI_SUPPORT.run( - 'config', - 'list', - f'--env={env}', - '--paged', - '--page-size=8', - '--output=json', - parse_json=True, - ).payload - risk_sets = self.risk_support.build_config_risk_sets(doctor_payload) - - if not isinstance(list_payload, dict) or not list_payload.get('ok', False): - return BrowserPageSnapshot( - title='参数配置', - subtitle=TUI_COPY.build_unavailable_subtitle( - '配置', - SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(list_payload), 72), - ), - records=[self.record_builder.build_failure_record(list_payload)], - shared_sections=[ - self.section_builder.build_configs_overview_section([], [], risk_sets, active_filter_label), - self.section_builder.build_config_doctor_section(doctor_payload), - self.section_builder.build_high_risk_config_section([], risk_sets), - self.section_builder.build_config_issue_samples_section(doctor_payload), - self.section_builder.build_config_set_entry_section([]), - ], - filters=list(self.filter_options), - active_filter_key=active_filter, - search=search_context, - ) - - rows = self.risk_support.append_orphan_cache_rows( - self.extract_page_rows(list_payload), - risk_sets.get('orphan-cache', set()), - ) - sorted_rows = sorted( - rows, key=lambda config_row: self.risk_support.build_config_sort_key(config_row, risk_sets) - ) - filtered_rows = self.risk_support.apply_config_query( - self.risk_support.apply_config_filter(sorted_rows, risk_sets, active_filter), - query, - ) - records = [ - self.record_builder.build_record( - config_row, - env, - risk_key=self.risk_support.resolve_config_risk_key( - str(config_row.get('configKey', '') or ''), - risk_sets, - ), - ) - for config_row in filtered_rows[:12] - ] - if not records: - records = [ - self.build_empty_record( - key='config:none', - subject='配置项', - empty_label='配置项', - has_source_rows=bool(sorted_rows), - filtered_summary='当前筛选条件下没有匹配配置项', - empty_summary='当前环境没有可浏览的参数配置项', - filtered_empty_value='暂无数据', - empty_empty_value='暂无配置', - filtered_detail='当前筛选条件下没有匹配配置项', - empty_detail='当前环境没有可浏览的参数配置项', - ) - ] - - subtitle = TUI_DIAGNOSTIC_SERVICE.build_configs_diagnostic_subtitle( - active_filter_label, - len(filtered_rows), - len(risk_sets.get('mismatch', set())), - len(risk_sets.get('missing-cache', set())) + len(risk_sets.get('orphan-cache', set())), - ) - if isinstance(doctor_payload, dict) and not doctor_payload.get('ok', False): - subtitle = ( - f'{subtitle} | {SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(doctor_payload), 72)}' - ) - shared_sections = [ - self.section_builder.build_configs_overview_section(rows, filtered_rows, risk_sets, active_filter_label), - self.section_builder.build_config_doctor_section(doctor_payload), - self.section_builder.build_high_risk_config_section(rows, risk_sets), - self.section_builder.build_config_issue_samples_section(doctor_payload), - self.section_builder.build_config_set_entry_section(rows), - ] - return BrowserPageSnapshot( - title='参数配置', - subtitle=subtitle, - records=records, - shared_sections=shared_sections, - filters=list(self.filter_options), - active_filter_key=active_filter, - search=search_context, - ) - - -CONFIGS_BROWSER_ADAPTER = ConfigsBrowserAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/crypto.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/crypto.py deleted file mode 100644 index 47c6f7a..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/crypto.py +++ /dev/null @@ -1,372 +0,0 @@ -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() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/database.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/database.py deleted file mode 100644 index 3e27912..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/database.py +++ /dev/null @@ -1,474 +0,0 @@ -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 DatabaseDetailSourcePayloads: - """ - 数据库详情页原始数据源快照。 - - :param revision_payload: `db current` 结果 - :param check_payload: `db check` 结果 - :param heads_payload: `db heads` 结果 - :param history_payload: `db history` 结果 - :param config_payload: `app config` 结果 - """ - - revision_payload: dict[str, Any] | None - check_payload: dict[str, Any] | None - heads_payload: dict[str, Any] | None - history_payload: dict[str, Any] | None - config_payload: dict[str, Any] | None - - -class DatabaseDetailSnapshotCollector: - """ - 数据库详情页数据采集器。 - - 该对象负责拉取数据库详情页所需的多路 CLI 原始结果, - 让 `DatabaseDetailAdapter` 保持详情页编排职责。 - """ - - def collect(self, env: str) -> DatabaseDetailSourcePayloads: - """ - 采集数据库详情页所需原始结果。 - - :param env: 当前运行环境 - :return: 数据库详情页原始数据源快照 - """ - return DatabaseDetailSourcePayloads( - revision_payload=NESTED_CLI_SUPPORT.run( - 'db', - 'current', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - check_payload=NESTED_CLI_SUPPORT.run( - 'db', - 'check', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - heads_payload=NESTED_CLI_SUPPORT.run( - 'db', - 'heads', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - history_payload=NESTED_CLI_SUPPORT.run( - 'db', - 'history', - f'--env={env}', - '--limit=8', - '--output=json', - parse_json=True, - ).payload, - config_payload=NESTED_CLI_SUPPORT.run( - 'app', - 'config', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - ) - - -class DatabaseSectionBuilder: - """ - 数据库详情分区构建器。 - - 该构建器负责将数据库相关 CLI 结果负载转换为 TUI 详情页分区, - 使详情页适配器本体只保留采集与编排职责。 - """ - - @staticmethod - def extract_revision_items(payload: dict[str, Any] | None) -> list[dict[str, Any]]: - """ - 从 Alembic 结果中提取修订版本列表。 - - :param payload: `db heads` 或 `db history` JSON 负载 - :return: 修订版本列表 - """ - items = payload.get('items') if isinstance(payload, dict) else None - if not isinstance(items, list): - return [] - return [item for item in items if isinstance(item, dict)] - - @staticmethod - def build_revision_item_lines(item: dict[str, Any], *, index: int, item_label: str) -> list[str]: - """ - 构建单条 Alembic 修订版本详情文本。 - - :param item: 修订版本字典 - :param index: 当前索引 - :param item_label: 条目标识名称 - :return: 文本行列表 - """ - down_revisions = item.get('downRevisions') - branch_labels = item.get('branchLabels') - depends_on = item.get('dependsOn') - return [ - f'## {item_label} {index:02d} · {item.get("revision", "-")}', - f'> 下游版本: {",".join(down_revisions) if isinstance(down_revisions, list) and down_revisions else "-"}', - f'> 分支标签: {",".join(branch_labels) if isinstance(branch_labels, list) and branch_labels else "-"}', - f'> 依赖版本: {",".join(depends_on) if isinstance(depends_on, list) and depends_on else "-"}', - f'> 说明: {SHELL_TEXT_FORMATTER.truncate_text(item.get("doc", "-"), 72)}', - f'> 文件: {SHELL_TEXT_FORMATTER.truncate_text(item.get("path", "-"), 72)}', - ] - - @staticmethod - def build_revision_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建数据库迁移状态分区。 - - :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 DetailSectionSnapshot( - title='迁移版本', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='迁移版本', empty_value=str(current_revision or '-') - ), - ) - return DetailSectionSnapshot( - title='迁移版本', - status='ok', - lines=[ - '## 当前版本', - f'迁移版本: {current_revision}', - '', - '## 状态说明', - f'结果消息: {TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(payload)}', - ], - ) - - def build_overview_section( - self, - revision_payload: dict[str, Any] | None, - check_payload: dict[str, Any] | None, - heads_payload: dict[str, Any] | None, - history_payload: dict[str, Any] | None, - ) -> DetailSectionSnapshot: - """ - 构建数据库页总览判断分区。 - - :param revision_payload: `db current` JSON 负载 - :param check_payload: `db check` JSON 负载 - :param heads_payload: `db heads` JSON 负载 - :param history_payload: `db history` JSON 负载 - :return: 分区快照 - """ - 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)) - current_revision = revision_payload.get('currentRevision', '-') if isinstance(revision_payload, dict) else '-' - head_items = self.extract_revision_items(heads_payload) - history_items = self.extract_revision_items(history_payload) - - status = 'ok' - conclusion = '数据库基线正常,可继续查看连接状态、Heads 与版本链路' - if not revision_ok or not check_ok: - status = 'fail' - conclusion = '数据库存在基础异常,优先确认迁移版本与连通性检查结果' - elif not heads_ok or len(head_items) != 1: - status = 'warn' - conclusion = '数据库存在迁移分叉风险,优先确认 Heads 与版本链路是否一致' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'当前 revision: {current_revision}', - f'连通性检查: {"正常" if check_ok else "异常"}', - f'Heads 数量: {len(head_items)}', - f'版本数量: {history_payload.get("totalCount", len(history_items)) if isinstance(history_payload, dict) else len(history_items)}', - '', - '## 建议入口', - '优先关注:迁移版本 / 连通性检查 / Heads 状态 / 升级入口', - ], - ) - - @staticmethod - def build_profile_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建数据库连接配置分区。 - - :param payload: `app config` JSON 负载 - :return: 分区快照 - """ - config = payload.get('config') if isinstance(payload, dict) else None - if not isinstance(config, dict): - return DetailSectionSnapshot( - title='连接信息', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='连接信息', empty_value='不可用' - ), - ) - return DetailSectionSnapshot( - title='连接信息', - status='ok', - lines=[ - '## 数据库连接', - f'数据库类型: {config.get("dbType", "-")}', - f'连接地址: {config.get("dbHost", "-")}:{config.get("dbPort", "-")}', - f'数据库名: {config.get("dbDatabase", "-")}', - ], - ) - - @staticmethod - def build_check_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建数据库连通性检查分区。 - - :param payload: `db check` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='连通性检查', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='数据库连接', empty_value='异常' - ), - ) - return DetailSectionSnapshot( - title='连通性检查', - status='ok', - lines=[ - '## 当前状态', - '数据库连接: 正常', - '', - '## 结果摘要', - f'结果消息: {TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(payload)}', - ], - ) - - def build_heads_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建 Alembic heads 分区。 - - :param payload: `db heads` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='Heads 状态', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='Heads', empty_value='不可用' - ), - ) - items = self.extract_revision_items(payload) - status = 'ok' if len(items) == 1 else 'warn' - lines = [ - '## Heads 概况', - f'Heads 数量: {len(items)}', - f'结果消息: {TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(payload)}', - ] - if not items: - lines.extend(['', '## Heads 列表', '> 当前仓库未返回可用 heads']) - else: - lines.extend(['', '## Heads 列表']) - for index, item in enumerate(items[:4], start=1): - lines.extend(self.build_revision_item_lines(item, index=index, item_label='Head')) - lines.append('') - if len(lines) > 1 and lines[-1] == '': - lines.pop() - return DetailSectionSnapshot( - title='Heads 状态', - status=status, - lines=lines, - ) - - def build_history_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建 Alembic 历史版本分区。 - - :param payload: `db history` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='历史版本', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='历史版本', empty_value='不可用' - ), - ) - items = self.extract_revision_items(payload) - lines = [ - '## 历史概况', - f'返回数量: {payload.get("count", len(items))}', - f'总版本数: {payload.get("totalCount", len(items))}', - f'查询上限: {payload.get("limit", len(items))}', - '', - '## 历史列表', - ] - if not items: - lines.append('> 当前仓库未返回可展示的历史版本') - else: - for index, item in enumerate(items[:6], start=1): - lines.extend(self.build_revision_item_lines(item, index=index, item_label='版本')) - lines.append('') - if lines[-1] == '': - lines.pop() - return DetailSectionSnapshot( - title='历史版本', - status='ok', - lines=lines, - ) - - @staticmethod - def build_upgrade_entry_section() -> DetailSectionSnapshot: - """ - 构建数据库升级向导入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='升级入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备执行数据库升级、校验迁移路径或在生产前做升级预演时,应先通过向导确认目标版本与 dry-run 结果。', - command=TUI_COPY.build_cli_command_hint('wizard', 'db-upgrade', '--output=text'), - guide='向导会继续询问目标环境和目标 revision,并在真正升级前先展示预览信息。', - ), - ) - - @staticmethod - def build_init_entry_section() -> DetailSectionSnapshot: - """ - 构建数据库初始化预演入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='初始化预演入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备初始化新库、重建本地环境,或先确认初始化命令会执行到哪些迁移版本时,应先执行 dry-run 预演。', - command=TUI_COPY.build_cli_command_hint('db', 'init', '--dry-run', '--output=text'), - guide='预演只返回命令摘要与执行目录;确认无误后,再通过外部终端或向导执行真实初始化流程。', - ), - ) - - def build_sections( - self, - *, - revision_payload: dict[str, Any] | None, - check_payload: dict[str, Any] | None, - heads_payload: dict[str, Any] | None, - history_payload: dict[str, Any] | None, - config_payload: dict[str, Any] | None, - ) -> list[DetailSectionSnapshot]: - """ - 构建数据库页全部详情分区。 - - :param revision_payload: `db current` 结果负载 - :param check_payload: `db check` 结果负载 - :param heads_payload: `db heads` 结果负载 - :param history_payload: `db history` 结果负载 - :param config_payload: `app config` 结果负载 - :return: 详情分区列表 - """ - return [ - self.build_overview_section(revision_payload, check_payload, heads_payload, history_payload), - self.build_revision_section(revision_payload), - self.build_profile_section(config_payload), - self.build_check_section(check_payload), - self.build_heads_section(heads_payload), - self.build_history_section(history_payload), - self.build_init_entry_section(), - self.build_upgrade_entry_section(), - ] - - -class DatabaseDetailAdapter(BaseDetailAdapter): - """ - 数据库详情页适配器。 - - 该适配器负责采集数据库相关 CLI 结果,并组装为 TUI 详情页快照。 - 页面私有的解析逻辑、分区构建逻辑和快照采集流程统一收口在该类中。 - - :param section_builder: 数据库详情分区构建器 - """ - - def __init__( - self, - section_builder: DatabaseSectionBuilder | None = None, - snapshot_collector: DatabaseDetailSnapshotCollector | None = None, - ) -> None: - """ - 初始化数据库详情页适配器。 - - :param section_builder: 数据库详情分区构建器 - :param snapshot_collector: 数据库详情页数据采集器 - :return: None - """ - super().__init__( - page_title='数据库', - search_view_key='database', - default_suggestions=[ - '总览判断', - '迁移版本', - '连接信息', - '连通性检查', - 'Heads 状态', - '历史版本', - '初始化预演入口', - '升级入口', - ], - ) - self.section_builder = section_builder or DatabaseSectionBuilder() - self.snapshot_collector = snapshot_collector or DatabaseDetailSnapshotCollector() - - 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( - revision_payload=source_payloads.revision_payload, - check_payload=source_payloads.check_payload, - heads_payload=source_payloads.heads_payload, - history_payload=source_payloads.history_payload, - config_payload=source_payloads.config_payload, - ) - return DetailPageSnapshot( - title='数据库', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_database_diagnostic_subtitle( - source_payloads.revision_payload, - source_payloads.check_payload, - source_payloads.heads_payload, - ), - sections=self.filter_sections(sections, query), - search=self.resolve_search_context(query), - ) - - -DATABASE_DETAIL_ADAPTER = DatabaseDetailAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/gen.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/gen.py deleted file mode 100644 index 37b7e44..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/gen.py +++ /dev/null @@ -1,807 +0,0 @@ -from typing import Any - -from cli.tui.adapters.base import BaseBrowserAdapter -from cli.tui.adapters.models import ( - TUI_ADAPTER_MODEL_RENDERER, - BrowserPageSnapshot, - BrowserRecordSnapshot, - 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 - - -class GenRenderingSupport: - """ - 代码生成浏览页渲染支持对象。 - - 该对象负责布尔语义渲染与常见文本辅助逻辑,供分区和记录构建复用。 - """ - - @staticmethod - def render_yes_no(value: object) -> str: - """ - 将常见布尔语义值转换为“是/否”。 - - :param value: 原始值 - :return: 中文布尔文本 - """ - normalized = str(value).strip().lower() - return '是' if normalized in {'1', 'y', 'yes', 'true'} else '否' - - -class GenSectionBuilder: - """ - 代码生成浏览页分区构建器。 - - 该构建器负责构建代码生成页共享分区,以及单条业务表详情分区。 - - :param page_adapter: 代码生成浏览页适配器 - :param rendering: 代码生成浏览页渲染支持对象 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - rendering: GenRenderingSupport, - ) -> None: - """ - 初始化代码生成浏览页分区构建器。 - - :param page_adapter: 代码生成浏览页适配器 - :param rendering: 代码生成浏览页渲染支持对象 - :return: None - """ - self.page_adapter = page_adapter - self.rendering = rendering - - def build_gen_focus_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建业务表概览分区。 - - :param payload: `gen detail` JSON 负载 - :return: 分区快照 - """ - detail = payload.get('detail') if isinstance(payload, dict) else None - info = detail.get('info') if isinstance(detail, dict) else None - if not isinstance(info, 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'表 ID: {payload.get("tableId", "-")}', - f'表名: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("tableName", "-"), 40)}', - ], - ) - - def build_gen_generation_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建代码生成配置分区。 - - :param payload: `gen detail` JSON 负载 - :return: 分区快照 - """ - detail = payload.get('detail') if isinstance(payload, dict) else None - info = detail.get('info') if isinstance(detail, dict) else None - if not isinstance(info, 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'生成类名: {SHELL_TEXT_FORMATTER.truncate_text(info.get("className", "-"), 32)}', - f'所属模块: {SHELL_TEXT_FORMATTER.truncate_text(info.get("moduleName", "-"), 24)}', - '', - '## 业务信息', - f'业务标识: {SHELL_TEXT_FORMATTER.truncate_text(info.get("businessName", "-"), 24)}', - f'功能名称: {SHELL_TEXT_FORMATTER.truncate_text(info.get("functionName", "-"), 24)}', - ], - ) - - def build_gen_column_summary_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建字段概览分区。 - - :param payload: `gen detail` JSON 负载 - :return: 分区快照 - """ - detail = payload.get('detail') if isinstance(payload, dict) else None - rows = detail.get('rows') if isinstance(detail, dict) else None - if not isinstance(rows, list): - return DetailSectionSnapshot( - title='字段摘要', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='字段摘要', empty_value='不可用' - ), - ) - row_dicts = [row for row in rows if isinstance(row, dict)] - primary_key_count = sum(1 for row in row_dicts if self.rendering.render_yes_no(row.get('isPk', '-')) == '是') - required_count = sum(1 for row in row_dicts if self.rendering.render_yes_no(row.get('isRequired', '-')) == '是') - return DetailSectionSnapshot( - title='字段摘要', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 字段规模', - f'字段总数: {payload.get("columnCount", "-")}', - f'主键字段: {primary_key_count}', - f'必填字段: {required_count}', - ], - ) - - def build_gen_columns_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建业务表字段分区。 - - :param payload: `gen detail` JSON 负载 - :return: 分区快照 - """ - detail = payload.get('detail') if isinstance(payload, dict) else None - rows = detail.get('rows') if isinstance(detail, dict) else None - if not isinstance(rows, list) or not rows: - return DetailSectionSnapshot( - title='字段列表', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='字段', empty_value='无'), - ) - lines: list[str] = [] - for row in rows[:12]: - if not isinstance(row, dict): - continue - lines.extend( - [ - f'## {SHELL_TEXT_FORMATTER.truncate_text(row.get("columnName", "-"), 24)} · {SHELL_TEXT_FORMATTER.truncate_text(row.get("columnType", "-"), 20)}', - ( - f'> 主键: {self.rendering.render_yes_no(row.get("isPk", "-"))} | ' - f'必填: {self.rendering.render_yes_no(row.get("isRequired", "-"))} | ' - f'查询: {row.get("queryType", "-")}' - ), - f'> 说明: {SHELL_TEXT_FORMATTER.truncate_text(row.get("columnComment", "-"), 40)}', - '', - ] - ) - return DetailSectionSnapshot( - title='字段列表', - status='ok' if payload.get('ok', False) else 'fail', - lines=(lines[:-1] if lines and lines[-1] == '' else lines) - or TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='字段信息', - empty_value='0 列', - detail='当前业务表没有可展示的字段信息', - ), - ) - - def build_gen_precheck_section( - self, - detail_payload: dict[str, Any] | None, - preview_payload: dict[str, Any] | None, - ) -> DetailSectionSnapshot: - """ - 构建代码生成前校验分区。 - - :param detail_payload: `gen detail` JSON 负载 - :param preview_payload: `gen preview` JSON 负载 - :return: 分区快照 - """ - detail = detail_payload.get('detail') if isinstance(detail_payload, dict) else None - info = detail.get('info') if isinstance(detail, dict) else None - rows = detail.get('rows') if isinstance(detail, dict) and isinstance(detail.get('rows'), list) else [] - preview = preview_payload.get('preview') if isinstance(preview_payload, dict) else None - preview_templates = preview if isinstance(preview, dict) else {} - row_dicts = [row for row in rows if isinstance(row, dict)] - - has_class_name = bool(isinstance(info, dict) and str(info.get('className', '') or '').strip()) - has_module_name = bool(isinstance(info, dict) and str(info.get('moduleName', '') or '').strip()) - has_business_name = bool(isinstance(info, dict) and str(info.get('businessName', '') or '').strip()) - has_primary_key = any(self.rendering.render_yes_no(row.get('isPk', '-')) == '是' for row in row_dicts) - column_count = len(row_dicts) - template_count = len(preview_templates) - - risk_items: list[str] = [] - if not has_class_name: - risk_items.append('生成类名缺失') - if not has_module_name: - risk_items.append('所属模块缺失') - if not has_business_name: - risk_items.append('业务标识缺失') - if column_count <= 0: - risk_items.append('字段列表为空') - if not has_primary_key: - risk_items.append('未识别到主键字段') - if template_count <= 0: - risk_items.append('未生成可预览模板') - - status = 'ok' if not risk_items else 'warn' - return DetailSectionSnapshot( - title='生成前校验', - status=status, - lines=[ - '## 校验结果', - f'生成类名: {"通过" if has_class_name else "缺失"}', - f'所属模块: {"通过" if has_module_name else "缺失"}', - f'业务标识: {"通过" if has_business_name else "缺失"}', - f'字段数量: {column_count}', - f'主键字段: {"通过" if has_primary_key else "缺失"}', - f'模板预览: {template_count} 份', - '', - '## 风险提示', - *(risk_items if risk_items else ['当前未发现阻断生成的明显风险']), - ], - ) - - def build_gen_sync_precheck_section( - self, - table_name: str, - payload: dict[str, Any] | None, - ) -> DetailSectionSnapshot: - """ - 构建数据库表结构同步前检查分区。 - - :param table_name: 业务表名称 - :param payload: `gen db-list --table-name=...` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='同步预检查', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='同步预检查', empty_value='不可用' - ), - ) - - rows = self.page_adapter.extract_page_rows(payload) - matched_row = next( - (row for row in rows if str(row.get('tableName', '') or '').strip().lower() == table_name.strip().lower()), - None, - ) - if matched_row is None: - return DetailSectionSnapshot( - title='同步预检查', - status='warn', - lines=[ - '## 当前状态', - f'目标业务表: {table_name}', - '数据库物理表: 未匹配', - '', - '## 建议操作', - '当前数据库中未找到同名物理表,执行同步前应先确认表结构来源', - ], - ) - - return DetailSectionSnapshot( - title='同步预检查', - status='ok', - lines=[ - '## 当前状态', - f'目标业务表: {table_name}', - f'数据库物理表: {matched_row.get("tableName", "-")}', - f'表注释: {SHELL_TEXT_FORMATTER.truncate_text(matched_row.get("tableComment", "-"), 56)}', - '', - '## 建议操作', - '已匹配到同名物理表,可继续使用同步数据库表结构动作', - ], - ) - - def build_gen_preview_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建代码预览分区。 - - :param payload: `gen preview` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='代码预览', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='代码预览', empty_value='不可用' - ), - ) - preview_payload = payload.get('preview') if isinstance(payload.get('preview'), dict) else {} - lines = [ - '## 模板规模', - f'模板数量: {payload.get("templateCount", len(preview_payload))}', - ] - if not preview_payload: - lines.extend( - [ - '', - *TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='模板样本', - empty_value='0 份', - detail='当前业务表没有可预览的模板输出', - ), - ] - ) - return DetailSectionSnapshot(title='代码预览', status='info', lines=lines) - - lines.extend(['', '## 模板样本']) - for template_name, template_content in list(preview_payload.items())[:4]: - preview_lines = str(template_content).splitlines()[:3] - lines.append(f'## {SHELL_TEXT_FORMATTER.truncate_text(template_name, 48)}') - lines.extend(f'> {SHELL_TEXT_FORMATTER.truncate_text(line, 72)}' for line in preview_lines) - lines.append('') - if lines[-1] == '': - lines.pop() - return DetailSectionSnapshot( - title='代码预览', - status='ok', - lines=lines, - ) - - def build_gen_export_preview_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建代码导出预演分区。 - - :param payload: `gen export --dry-run` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='导出预览', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='导出预览', empty_value='不可用' - ), - ) - - results = payload.get('results') if isinstance(payload.get('results'), list) else [] - lines = [ - '## 预演结果', - f'执行模式: {payload.get("mode", "-")}', - f'dry-run: {"是" if payload.get("dryRun", False) else "否"}', - f'表数量: {len(payload.get("tableNames", [])) if isinstance(payload.get("tableNames"), list) else 0}', - f'结果数量: {len(results)}', - '', - '## 摘要', - f'结果摘要: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}', - ] - if payload.get('outputFile'): - lines.append(f'输出文件: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("outputFile", "-"), 72)}') - if payload.get('genPath'): - lines.append(f'输出目录: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("genPath", "-"), 72)}') - if results: - lines.extend(['', '## 结果样本']) - for item in results[:4]: - if not isinstance(item, dict): - continue - lines.append( - f'> {item.get("tableName", "-")} · {"成功" if item.get("ok", False) else "失败"} · ' - f'{SHELL_TEXT_FORMATTER.truncate_text(item.get("message", "-"), 48)}' - ) - return DetailSectionSnapshot( - title='导出预览', - status='ok', - lines=lines, - ) - - def build_importable_tables_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建数据库可导入表共享分区。 - - :param payload: `gen db-list` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='可导入数据表', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='数据表', empty_value='不可用' - ), - ) - rows = self.page_adapter.extract_page_rows(payload) - lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='可导入数据表', - empty_value='0 张', - detail='当前环境没有可导入的数据库表', - ) - if rows: - lines = [ - f'{SHELL_TEXT_FORMATTER.truncate_text(row.get("tableName", "-"), 40)} · {SHELL_TEXT_FORMATTER.truncate_text(row.get("tableComment", "-"), 48)}' - for row in rows[:8] - ] - return DetailSectionSnapshot( - title='可导入数据表', - status='ok', - lines=lines, - ) - - @staticmethod - def build_gen_export_entry_section(table_name: str) -> DetailSectionSnapshot: - """ - 构建代码导出向导入口分区。 - - :param table_name: 当前业务表名称 - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='导出入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario=f'准备导出当前业务表 {table_name} 的生成结果时,应先通过向导确认目标表、输出目录和覆盖范围。', - command=TUI_COPY.build_cli_command_hint('wizard', 'gen-export', '--output=text'), - guide='进入向导后优先选择当前业务表,并在最终确认前检查导出目录和文件覆盖预览。', - ), - ) - - @staticmethod - def build_gen_import_entry_section(importable_rows: list[dict[str, Any]]) -> DetailSectionSnapshot: - """ - 构建物理表导入入口分区。 - - :param importable_rows: 可导入物理表列表 - :return: 分区快照 - """ - suggested_table_names = [ - str(row.get('tableName', '') or '').strip() - for row in importable_rows[:3] - if str(row.get('tableName', '') or '').strip() - ] - suggested_tables = ' '.join(suggested_table_names) if suggested_table_names else '' - return DetailSectionSnapshot( - title='导入入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备把数据库物理表纳入代码生成管理时,应先核对物理表名、注释和目标环境,再通过导入命令执行 dry-run 预演。', - command=TUI_COPY.build_cli_command_hint( - 'gen', 'import-table', suggested_tables, '--dry-run', '--output=text' - ), - guide='建议先从“可导入数据表”中确认目标表名,必要时一次只导入 1 到 3 张表,确认 dry-run 输出后再执行真实导入。', - ), - ) - - @staticmethod - def build_gen_create_entry_section() -> DetailSectionSnapshot: - """ - 构建建表 SQL 导入入口分区。 - - :return: 分区快照 - """ - sql_template = 'CREATE TABLE demo_table (id bigint primary key);' - return DetailSectionSnapshot( - title='建表入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备根据建表 SQL 直接创建物理表并导入代码生成配置时,应先使用单条 SQL 执行 dry-run 预演,确认语句可解析且只包含目标表。', - command=TUI_COPY.build_cli_command_hint( - 'gen', 'create-table', '--dry-run', '--sql', sql_template, '--output=text' - ), - guide='建议优先使用单条 CREATE TABLE 语句做预演;若 SQL 较长或来自文件,可改用 --sql-file 方式,但不要同时传入 --sql 与 --sql-file。', - ), - ) - - def load_gen_detail_sections( - self, - table_row: dict[str, Any], - env: str, - ) -> list[DetailSectionSnapshot]: - """ - 按需加载单条业务表详情分区。 - - :param table_row: 业务表列表行数据 - :param env: 当前运行环境 - :return: 详情分区列表 - """ - table_id = table_row.get('tableId') - detail_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'detail', - str(table_id), - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - preview_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'preview', - str(table_id), - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - table_name = str(table_row.get('tableName', '-') or '-') - export_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'export', - table_name, - f'--env={env}', - '--dry-run', - '--mode=zip', - '--output=json', - '--yes', - parse_json=True, - ).payload - sync_check_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'db-list', - f'--env={env}', - f'--table-name={table_name}', - '--paged', - '--page-size=5', - '--output=json', - parse_json=True, - ).payload - return [ - self.build_gen_focus_section(detail_payload), - self.build_gen_generation_section(detail_payload), - self.build_gen_column_summary_section(detail_payload), - self.build_gen_precheck_section(detail_payload, preview_payload), - self.build_gen_sync_precheck_section(table_name, sync_check_payload), - self.build_gen_columns_section(detail_payload), - self.build_gen_preview_section(preview_payload), - self.build_gen_export_preview_section(export_payload), - self.build_gen_export_entry_section(table_name), - ] - - def build_gen_overview_section( - self, - gen_rows: list[dict[str, Any]], - filtered_rows: list[dict[str, Any]], - importable_rows: list[dict[str, Any]], - ) -> DetailSectionSnapshot: - """ - 构建代码生成页总览判断共享分区。 - - :param gen_rows: 全量业务表行列表 - :param filtered_rows: 当前筛选后的业务表行列表 - :param importable_rows: 可导入物理表行列表 - :return: 分区快照 - """ - incomplete_count = sum( - 1 - for row in gen_rows - if not str(row.get('className', '') or '').strip() - or not str(row.get('moduleName', '') or '').strip() - or not str(row.get('businessName', '') or '').strip() - ) - status = 'ok' - conclusion = '当前生成配置可继续下钻,优先查看预检查、代码预览和导出预演' - if incomplete_count > 0: - status = 'warn' - conclusion = '存在生成配置不完整的业务表,建议先处理生成前校验风险' - elif not gen_rows and importable_rows: - status = 'info' - conclusion = '当前还没有业务表配置,可先从可导入物理表中选择目标' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'已加载业务表: {len(gen_rows)} 张', - f'当前匹配: {len(filtered_rows)} 张', - f'可导入物理表: {len(importable_rows)} 张', - f'配置待补全: {incomplete_count} 张', - '', - '## 建议入口', - '优先关注:生成前校验 / 同步预检查 / 代码预览 / 导出预览 / 导入入口 / 建表入口', - ], - ) - - -class GenRecordBuilder: - """ - 代码生成浏览记录构建器。 - - 该对象负责构建代码生成页单条业务表浏览记录与失败兜底记录。 - - :param page_adapter: 代码生成浏览页适配器 - :param section_builder: 代码生成浏览页分区构建器 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - section_builder: GenSectionBuilder, - ) -> None: - """ - 初始化代码生成浏览记录构建器。 - - :param page_adapter: 代码生成浏览页适配器 - :param section_builder: 代码生成浏览页分区构建器 - :return: None - """ - self.page_adapter = page_adapter - self.section_builder = section_builder - - def build_gen_record(self, table_row: dict[str, Any], env: str) -> BrowserRecordSnapshot: - """ - 构建单条业务表浏览记录。 - - :param table_row: 业务表列表行数据 - :param env: 当前运行环境 - :return: 浏览记录快照 - """ - table_id = table_row.get('tableId', '-') - table_name = str(table_row.get('tableName', '-') or '-') - class_name = str(table_row.get('className', '-') or '-') - module_name = str(table_row.get('moduleName', '-') or '-') - - return BrowserRecordSnapshot( - key=f'gen:{table_id}', - title=SHELL_TEXT_FORMATTER.truncate_text(table_name, 40), - status='ok', - summary=f'生成类 {SHELL_TEXT_FORMATTER.truncate_text(class_name, 24)} · 模块 {SHELL_TEXT_FORMATTER.truncate_text(module_name, 20)}', - metadata_lines=[ - '## 表身份', - f'表 ID: {table_id}', - '', - '## 生成信息', - f'生成类名: {SHELL_TEXT_FORMATTER.truncate_text(class_name, 32)}', - f'所属模块: {SHELL_TEXT_FORMATTER.truncate_text(module_name, 24)}', - f'业务标识: {SHELL_TEXT_FORMATTER.truncate_text(table_row.get("businessName", "-"), 24)}', - ], - detail_sections=[], - detail_loader=lambda table_row=table_row, env=env: self.section_builder.load_gen_detail_sections( - table_row, env - ), - ) - - def build_failure_record(self, payload: dict[str, Any] | None) -> BrowserRecordSnapshot: - """ - 构建代码生成页失败兜底记录。 - - :param payload: 失败结果负载 - :return: 浏览记录快照 - """ - return self.page_adapter.build_failure_record( - key='gen:unavailable', - subject='代码生成', - section_subject='业务表列表', - payload=payload, - ) - - -class GenBrowserAdapter(BaseBrowserAdapter): - """ - 代码生成浏览页适配器。 - - 该适配器负责采集业务表列表、详情、预览与可导入表清单,并委托 - 协作对象完成共享分区、详情分区和记录构建。 - """ - - def __init__( - self, - rendering: GenRenderingSupport | None = None, - section_builder: GenSectionBuilder | None = None, - record_builder: GenRecordBuilder | None = None, - ) -> None: - """ - 初始化代码生成浏览页适配器。 - - :param rendering: 代码生成浏览页渲染支持对象 - :param section_builder: 代码生成浏览页分区构建器 - :param record_builder: 代码生成浏览记录构建器 - :return: None - """ - super().__init__( - page_title='代码生成', - search_view_key='gen', - filter_options=(), - ) - self.rendering = rendering or GenRenderingSupport() - self.section_builder = section_builder or GenSectionBuilder(self, self.rendering) - self.record_builder = record_builder or GenRecordBuilder(self, self.section_builder) - - @staticmethod - def apply_gen_query(rows: list[dict[str, Any]], query: str) -> list[dict[str, Any]]: - """ - 按业务表名、生成类名或模块名查询词过滤业务表行数据。 - - :param rows: 原始业务表行列表 - :param query: 当前搜索词 - :return: 过滤后的业务表行列表 - """ - normalized_query = str(query).strip().lower() - if not normalized_query: - return rows - return [ - row - for row in rows - if normalized_query in str(row.get('tableName', '') or '').strip().lower() - or normalized_query in str(row.get('className', '') or '').strip().lower() - or normalized_query in str(row.get('moduleName', '') or '').strip().lower() - ] - - def collect_snapshot(self, env: str, query: str = '') -> BrowserPageSnapshot: - """ - 采集代码生成浏览页只读快照。 - - :param env: 当前运行环境 - :param query: 当前搜索词 - :return: 浏览页快照 - """ - search_context = self.resolve_search_context(query) - gen_tables_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'list', - f'--env={env}', - '--paged', - '--page-size=8', - '--output=json', - parse_json=True, - ).payload - db_tables_payload = NESTED_CLI_SUPPORT.run( - 'gen', - 'db-list', - f'--env={env}', - '--paged', - '--page-size=8', - '--output=json', - parse_json=True, - ).payload - importable_rows = self.extract_page_rows(db_tables_payload) - - if not isinstance(gen_tables_payload, dict) or not gen_tables_payload.get('ok', False): - return BrowserPageSnapshot( - title='代码生成', - subtitle=TUI_COPY.build_unavailable_subtitle( - '业务表', - SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(gen_tables_payload), 72), - ), - records=[self.record_builder.build_failure_record(gen_tables_payload)], - shared_sections=[ - self.section_builder.build_gen_overview_section([], [], importable_rows), - self.section_builder.build_importable_tables_section(db_tables_payload), - self.section_builder.build_gen_import_entry_section(importable_rows), - self.section_builder.build_gen_create_entry_section(), - ], - search=search_context, - ) - - rows = self.extract_page_rows(gen_tables_payload) - filtered_rows = self.apply_gen_query(rows, query) - records = [self.record_builder.build_gen_record(table_row, env) for table_row in filtered_rows[:8]] - if not records: - records = [ - self.build_empty_record( - key='gen:none', - subject='业务表', - empty_label='业务表', - has_source_rows=bool(rows), - filtered_summary='当前搜索条件下没有匹配业务表', - empty_summary='当前环境中还没有纳入代码生成的业务表', - filtered_empty_value='暂无匹配', - empty_empty_value='暂无配置', - filtered_detail='当前搜索条件下没有匹配业务表', - empty_detail='当前环境中尚未配置代码生成业务表', - ) - ] - - return BrowserPageSnapshot( - title='代码生成', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_gen_diagnostic_subtitle( - len(filtered_rows), - len(importable_rows), - ), - records=records, - shared_sections=[ - self.section_builder.build_gen_overview_section(rows, filtered_rows, importable_rows), - self.section_builder.build_importable_tables_section(db_tables_payload), - self.section_builder.build_gen_import_entry_section(importable_rows), - self.section_builder.build_gen_create_entry_section(), - ], - search=search_context, - ) - - -GEN_BROWSER_ADAPTER = GenBrowserAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/health.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/health.py deleted file mode 100644 index 8aefde4..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/health.py +++ /dev/null @@ -1,988 +0,0 @@ -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() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/jobs.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/jobs.py deleted file mode 100644 index d5b3e5b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/jobs.py +++ /dev/null @@ -1,747 +0,0 @@ -from typing import Any - -from cli.tui.adapters.base import BaseBrowserAdapter -from cli.tui.adapters.models import ( - TUI_ADAPTER_MODEL_RENDERER, - BrowserPageSnapshot, - BrowserRecordSnapshot, - DetailSectionSnapshot, -) -from cli.tui.copy import TUI_COPY -from cli.tui.diagnostics import TUI_DIAGNOSTIC_SERVICE -from cli.tui.search import JOB_FILTER_OPTIONS -from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER - - -class JobRenderingSupport: - """ - 任务浏览页渲染支持对象。 - - 该对象负责状态文本、时间线标记、成功率信号条和失败任务提取等基础 - 逻辑,供筛选、分区和记录构建复用。 - - :param page_adapter: 任务浏览页适配器 - """ - - def __init__(self, page_adapter: BaseBrowserAdapter) -> None: - """ - 初始化任务浏览页渲染支持对象。 - - :param page_adapter: 任务浏览页适配器 - :return: None - """ - self.page_adapter = page_adapter - - def extract_failed_job_names(self, payload: dict[str, Any] | None) -> set[str]: - """ - 从失败日志结果中提取任务名称集合。 - - :param payload: 失败日志负载 - :return: 失败任务名称集合 - """ - rows = self.page_adapter.extract_page_rows(payload) - failed_job_names: set[str] = set() - for row in rows: - job_name = str(row.get('jobName', '') or '').strip() - if job_name: - failed_job_names.add(job_name) - return failed_job_names - - @staticmethod - def render_job_status(value: object) -> str: - """ - 将任务状态码转换为可读文本。 - - :param value: 原始状态值 - :return: 中文状态文本 - """ - normalized = str(value).strip() - if normalized == '0': - return '正常' - if normalized == '1': - return '暂停' - return normalized or '-' - - @staticmethod - def render_job_log_status(value: object) -> str: - """ - 将任务日志状态码转换为可读文本。 - - :param value: 原始状态值 - :return: 中文状态文本 - """ - normalized = str(value).strip() - if normalized == '0': - return '成功' - if normalized == '1': - return '失败' - return normalized or '-' - - def render_job_log_timeline_title(self, row: dict[str, Any]) -> str: - """ - 渲染单条任务日志时间线标题。 - - :param row: 日志行数据 - :return: 时间线标题文本 - """ - created_at = SHELL_TEXT_FORMATTER.truncate_text(row.get('createTime', '-'), 24) - if created_at and created_at != '-': - return f'{created_at} · {self.render_job_log_status(row.get("status", "-"))}' - return f'日志 {row.get("jobLogId", "-")} · {self.render_job_log_status(row.get("status", "-"))}' - - @staticmethod - def render_timeline_marker(status: object) -> str: - """ - 根据日志状态渲染时间线节点标记。 - - :param status: 原始日志状态 - :return: 节点标记 - """ - return 'x' if str(status).strip() == '1' else 'o' - - @staticmethod - def render_signal_bar(passed: int, total: int, *, width: int = 8) -> str: - """ - 渲染 ASCII 成功率条。 - - :param passed: 成功数量 - :param total: 总数量 - :param width: 条形宽度 - :return: ASCII 条形 - """ - if total <= 0: - return f'[{"-" * width}]' - safe_passed = max(0, min(passed, total)) - filled = round((safe_passed / total) * width) - return f'[{"#" * filled}{"-" * max(0, width - filled)}]' - - @staticmethod - def extract_latest_log_time(rows: list[dict[str, Any]]) -> str: - """ - 提取日志列表中的最近时间。 - - :param rows: 日志行数据 - :return: 最近时间文本 - """ - if not rows: - return '-' - latest_time = str(rows[0].get('createTime', '-') or '-').strip() - return latest_time or '-' - - -class JobRowFilter: - """ - 任务浏览行过滤器。 - - 该对象负责按筛选键和搜索词过滤任务行数据。 - """ - - @staticmethod - def apply_job_filter( - rows: list[dict[str, Any]], - failed_job_names: set[str], - filter_key: str, - ) -> list[dict[str, Any]]: - """ - 按筛选键过滤任务行数据。 - - :param rows: 原始任务行列表 - :param failed_job_names: 最近失败任务名称集合 - :param filter_key: 当前筛选键 - :return: 过滤后的任务行列表 - """ - normalized_filter = str(filter_key).strip().lower() - if normalized_filter == 'failed': - return [row for row in rows if str(row.get('jobName', '') or '').strip() in failed_job_names] - if normalized_filter == 'paused': - return [row for row in rows if str(row.get('status', '')).strip() == '1'] - if normalized_filter == 'ok': - return [ - row - for row in rows - if str(row.get('jobName', '') or '').strip() not in failed_job_names - and str(row.get('status', '')).strip() != '1' - ] - return rows - - @staticmethod - def apply_job_query(rows: list[dict[str, Any]], query: str) -> list[dict[str, Any]]: - """ - 按任务名称查询词过滤任务行数据。 - - :param rows: 原始任务行列表 - :param query: 当前搜索词 - :return: 过滤后的任务行列表 - """ - normalized_query = str(query).strip().lower() - if not normalized_query: - return rows - return [ - row - for row in rows - if normalized_query in str(row.get('jobName', '') or '').strip().lower() - or normalized_query in str(row.get('jobGroup', '') or '').strip().lower() - ] - - -class JobSectionBuilder: - """ - 任务浏览页分区构建器。 - - 该构建器负责构建任务页共享分区以及单条任务详情分区。 - - :param page_adapter: 任务浏览页适配器 - :param rendering: 任务浏览页渲染支持对象 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - rendering: JobRenderingSupport, - ) -> None: - """ - 初始化任务浏览页分区构建器。 - - :param page_adapter: 任务浏览页适配器 - :param rendering: 任务浏览页渲染支持对象 - :return: None - """ - self.page_adapter = page_adapter - self.rendering = rendering - - def build_job_failure_aggregate_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建任务失败聚合共享分区。 - - :param payload: 失败日志 JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title='失败聚合', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='失败聚合', empty_value='不可用' - ), - ) - - rows = self.page_adapter.extract_page_rows(payload) - if not rows: - return DetailSectionSnapshot( - title='失败聚合', - status='ok', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='失败日志', - empty_value='0 条', - detail='当前失败日志池为空,最近没有采集到失败执行记录', - ), - ) - - grouped_rows: dict[str, list[dict[str, Any]]] = {} - for row in rows: - job_name = str(row.get('jobName', '-') or '-').strip() or '-' - grouped_rows.setdefault(job_name, []).append(row) - - sorted_groups = sorted( - grouped_rows.items(), - key=lambda item: (-len(item[1]), self.rendering.extract_latest_log_time(item[1]), item[0]), - reverse=False, - ) - lines = [ - '## 失败池', - f'失败日志: {len(rows)} 条', - f'涉及任务: {len(grouped_rows)} 个', - f'最近失败: {self.rendering.extract_latest_log_time(rows)}', - '', - '## 高频失败任务', - ] - for index, (job_name, job_rows) in enumerate(sorted_groups[:6], start=1): - latest_row = job_rows[0] - latest_time = self.rendering.extract_latest_log_time(job_rows) - latest_message = SHELL_TEXT_FORMATTER.truncate_text(latest_row.get('jobMessage', '-'), 48) - lines.extend( - [ - f'[{index}] {SHELL_TEXT_FORMATTER.truncate_text(job_name, 28)} · {len(job_rows)} 次', - f'> 最近失败: {latest_time}', - f'> 最近结果: {latest_message}', - ] - ) - exception_info = SHELL_TEXT_FORMATTER.truncate_text(latest_row.get('exceptionInfo', ''), 48) - if exception_info: - lines.append(f'> 异常摘要: {exception_info}') - lines.append('') - - if lines[-1] == '': - lines.pop() - return DetailSectionSnapshot( - title='失败聚合', - status='warn', - lines=lines, - ) - - def build_jobs_overview_section( - self, - rows: list[dict[str, Any]], - filtered_rows: list[dict[str, Any]], - failed_job_names: set[str], - paused_count: int, - failed_logs_payload: dict[str, Any] | None, - filter_label: str, - ) -> DetailSectionSnapshot: - """ - 构建任务页总览判断共享分区。 - - :param rows: 原始任务行列表 - :param filtered_rows: 当前筛选后的任务行列表 - :param failed_job_names: 最近失败任务名称集合 - :param paused_count: 暂停任务数量 - :param failed_logs_payload: 失败日志负载 - :param filter_label: 当前筛选标签 - :return: 分区快照 - """ - failed_rows = self.page_adapter.extract_page_rows(failed_logs_payload) - latest_failure = self.rendering.extract_latest_log_time(failed_rows) - status = 'ok' - conclusion = '当前任务基线正常,可继续查看执行轨迹与最近执行记录' - if failed_job_names: - status = 'warn' - conclusion = '存在失败任务,优先查看失败聚合与失败执行记录' - elif paused_count > 0: - status = 'warn' - conclusion = '存在暂停任务,建议确认停用原因与恢复窗口' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'当前筛选: {filter_label}', - f'已加载任务: {len(rows)} 条', - f'当前匹配: {len(filtered_rows)} 条', - f'失败任务: {len(failed_job_names)} 个', - f'暂停任务: {paused_count} 个', - f'最近失败: {latest_failure}', - '', - '## 建议入口', - '优先关注:失败聚合 / 暂停任务 / 执行轨迹', - ], - ) - - def build_job_focus_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建任务概览分区。 - - :param payload: `job detail` JSON 负载 - :return: 分区快照 - """ - job = payload.get('job') if isinstance(payload, dict) else None - if not isinstance(job, 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'任务 ID: {job.get("jobId", "-")}', - f'任务名称: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobName", "-"), 40)}', - f'任务分组: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobGroup", "-"), 24)}', - f'运行状态: {self.rendering.render_job_status(job.get("status", "-"))}', - ], - ) - - @staticmethod - def build_job_schedule_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建任务调度配置分区。 - - :param payload: `job detail` JSON 负载 - :return: 分区快照 - """ - job = payload.get('job') if isinstance(payload, dict) else None - if not isinstance(job, 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'Cron 表达式: {SHELL_TEXT_FORMATTER.truncate_text(job.get("cronExpression", "-"), 48)}', - '', - '## 执行入口', - f'调用目标: {SHELL_TEXT_FORMATTER.truncate_text(job.get("invokeTarget", "-"), 72)}', - ], - ) - - def build_job_logs_section(self, payload: dict[str, Any] | None, *, title: str) -> DetailSectionSnapshot: - """ - 构建任务日志分区。 - - :param payload: `job logs` JSON 负载 - :param title: 分区标题 - :return: 分区快照 - """ - if not isinstance(payload, dict) or not payload.get('ok', False): - return DetailSectionSnapshot( - title=title, - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines(payload, empty_label='日志', empty_value='不可用'), - ) - rows = self.page_adapter.extract_page_rows(payload) - lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='日志记录', - empty_value='0 条', - detail='当前筛选条件下没有相关日志', - ) - section_status = 'ok' - if rows: - lines = [] - if any(str(row.get('status', '')).strip() == '1' for row in rows): - section_status = 'warn' - for row in rows[:8]: - exception_info = SHELL_TEXT_FORMATTER.truncate_text(row.get('exceptionInfo', '-'), 56) - timeline_marker = self.rendering.render_timeline_marker(row.get('status', '-')) - rendered_status = self.rendering.render_job_log_status(row.get('status', '-')) - lines.extend( - [ - f'## 节点 {row.get("jobLogId", "-")} · {self.rendering.render_job_log_timeline_title(row)}', - f'> {timeline_marker} 状态: {rendered_status}', - f'> 时间: {SHELL_TEXT_FORMATTER.truncate_text(row.get("createTime", "-"), 24)}', - f'> 任务: {SHELL_TEXT_FORMATTER.truncate_text(row.get("jobName", "-"), 24)}', - f'> 结果: {SHELL_TEXT_FORMATTER.truncate_text(row.get("jobMessage", "-"), 56)}', - *([f'> 异常: {exception_info}'] if exception_info and exception_info != '-' else []), - '> 轨道: ├─采集 ├─执行 └─落盘', - f'> 轨迹: {"●" if timeline_marker == "o" else "▲"}─{"●" if timeline_marker == "o" else "▲"}─◎', - '', - ] - ) - return DetailSectionSnapshot( - title=title, - status=section_status, - lines=lines[:-1] if len(lines) > 1 and lines[-1] == '' else lines, - ) - - def build_job_log_summary_section( - self, - recent_logs_payload: dict[str, Any] | None, - failed_logs_payload: dict[str, Any] | None, - ) -> DetailSectionSnapshot: - """ - 构建任务日志摘要分区。 - - :param recent_logs_payload: 最近日志 JSON 负载 - :param failed_logs_payload: 失败日志 JSON 负载 - :return: 分区快照 - """ - if not isinstance(recent_logs_payload, dict) or not recent_logs_payload.get('ok', False): - return DetailSectionSnapshot( - title='执行摘要', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - recent_logs_payload, empty_label='日志摘要', empty_value='不可用' - ), - ) - recent_rows = self.page_adapter.extract_page_rows(recent_logs_payload) - failed_rows = self.page_adapter.extract_page_rows(failed_logs_payload) - success_count = sum(1 for row in recent_rows if str(row.get('status', '')).strip() == '0') - failed_count = sum(1 for row in recent_rows if str(row.get('status', '')).strip() not in {'', '0'}) - status = 'warn' if failed_count > 0 or failed_rows else 'ok' - total_count = len(recent_rows) - latest_run = self.rendering.extract_latest_log_time(recent_rows) - latest_failure = self.rendering.extract_latest_log_time(failed_rows) - return DetailSectionSnapshot( - title='执行摘要', - status=status, - lines=[ - '## 执行统计', - f'最近采样日志: {total_count} 条', - f'执行成功: {success_count} 条', - f'执行失败: {failed_count} 条', - f'成功率信号: {self.rendering.render_signal_bar(success_count, total_count)} {success_count}/{total_count or 0}', - '', - '## 时间线锚点', - f'最近一次执行: {latest_run}', - f'最近一次失败: {latest_failure}', - '', - '## 轨道视图', - f'执行轨道: {"─".join("●" if str(row.get("status", "")).strip() == "0" else "▲" for row in recent_rows[:6]) or "-"}', - f'失败轨道: {"─".join("▲" for _ in failed_rows[:6]) or "-"}', - f'轨道窗口: {"[执行流]" if recent_rows else "-"} {"[失败流]" if failed_rows else ""}'.strip(), - '', - '## 风险信号', - f'失败记录池: {len(failed_rows)} 条', - ], - ) - - def load_job_detail_sections(self, job_row: dict[str, Any], env: str) -> list[DetailSectionSnapshot]: - """ - 按需加载单条任务详情与日志分区。 - - :param job_row: 任务列表行数据 - :param env: 当前运行环境 - :return: 详情分区列表 - """ - job_id = job_row.get('jobId', '-') - job_name = str(job_row.get('jobName', '-') or '-') - - detail_payload = NESTED_CLI_SUPPORT.run( - 'job', - 'detail', - str(job_id), - f'--env={env}', - '--output=json', - parse_json=True, - ).payload - - log_arguments = ['job', 'logs', f'--env={env}', '--paged', '--page-size=8', '--output=json'] - if job_name.strip() and job_name != '-': - log_arguments.append(f'--job-name={job_name}') - recent_logs_payload = NESTED_CLI_SUPPORT.run(*log_arguments, parse_json=True).payload - failed_logs_payload = NESTED_CLI_SUPPORT.run(*[*log_arguments, '--status=1'], parse_json=True).payload - - return [ - self.build_job_focus_section(detail_payload), - self.build_job_schedule_section(detail_payload), - self.build_job_log_summary_section(recent_logs_payload, failed_logs_payload), - self.build_job_logs_section(recent_logs_payload, title='最近执行记录'), - self.build_job_logs_section(failed_logs_payload, title='失败执行记录'), - ] - - -class JobRecordBuilder: - """ - 任务浏览记录构建器。 - - 该对象负责构建任务页单条浏览记录与失败兜底记录。 - - :param page_adapter: 任务浏览页适配器 - :param rendering: 任务浏览页渲染支持对象 - :param section_builder: 任务浏览页分区构建器 - """ - - def __init__( - self, - page_adapter: BaseBrowserAdapter, - rendering: JobRenderingSupport, - section_builder: JobSectionBuilder, - ) -> None: - """ - 初始化任务浏览记录构建器。 - - :param page_adapter: 任务浏览页适配器 - :param rendering: 任务浏览页渲染支持对象 - :param section_builder: 任务浏览页分区构建器 - :return: None - """ - self.page_adapter = page_adapter - self.rendering = rendering - self.section_builder = section_builder - - def build_job_record(self, job_row: dict[str, Any], env: str) -> BrowserRecordSnapshot: - """ - 构建单条任务浏览记录。 - - :param job_row: 任务列表行数据 - :param env: 当前运行环境 - :return: 浏览记录快照 - """ - job_id = job_row.get('jobId', '-') - job_name = str(job_row.get('jobName', '-') or '-') - job_status = str(job_row.get('status', '-') or '-') - job_group = str(job_row.get('jobGroup', '-') or '-') - cron_expression = str(job_row.get('cronExpression', '-') or '-') - rendered_status = self.rendering.render_job_status(job_status) - - return BrowserRecordSnapshot( - key=f'job:{job_id}', - title=SHELL_TEXT_FORMATTER.truncate_text(job_name, 40), - status='warn' if str(job_status).strip() == '1' else 'ok', - summary=f'{rendered_status} · Cron {SHELL_TEXT_FORMATTER.truncate_text(cron_expression, 32)}', - metadata_lines=[ - '## 任务标识', - f'任务 ID: {job_id}', - f'任务分组: {SHELL_TEXT_FORMATTER.truncate_text(job_group, 24)}', - f'运行状态: {rendered_status}', - '', - '## 调度信息', - f'Cron 表达式: {SHELL_TEXT_FORMATTER.truncate_text(cron_expression, 48)}', - ], - detail_sections=[], - detail_loader=lambda job_row=job_row, env=env: self.section_builder.load_job_detail_sections(job_row, env), - ) - - def build_failure_record(self, payload: dict[str, Any] | None) -> BrowserRecordSnapshot: - """ - 构建任务页失败兜底记录。 - - :param payload: 失败结果负载 - :return: 浏览记录快照 - """ - return self.page_adapter.build_failure_record( - key='job:unavailable', - subject='任务', - section_subject='任务列表', - payload=payload, - ) - - -class JobsBrowserAdapter(BaseBrowserAdapter): - """ - 任务浏览页适配器。 - - 该适配器负责采集任务列表、失败日志和单条任务详情,并委托协作对象 - 完成过滤、共享分区、详情分区和记录构建。 - """ - - def __init__( - self, - rendering: JobRenderingSupport | None = None, - row_filter: JobRowFilter | None = None, - section_builder: JobSectionBuilder | None = None, - record_builder: JobRecordBuilder | None = None, - ) -> None: - """ - 初始化任务浏览页适配器。 - - :param rendering: 任务浏览页渲染支持对象 - :param row_filter: 任务浏览行过滤器 - :param section_builder: 任务浏览页分区构建器 - :param record_builder: 任务浏览记录构建器 - :return: None - """ - super().__init__( - page_title='任务', - search_view_key='jobs', - filter_options=JOB_FILTER_OPTIONS, - ) - self.rendering = rendering or JobRenderingSupport(self) - self.row_filter = row_filter or JobRowFilter() - self.section_builder = section_builder or JobSectionBuilder(self, self.rendering) - self.record_builder = record_builder or JobRecordBuilder(self, self.rendering, self.section_builder) - - def collect_snapshot(self, env: str, filter_key: str = 'all', query: str = '') -> BrowserPageSnapshot: - """ - 采集定时任务浏览页只读快照。 - - :param env: 当前运行环境 - :param filter_key: 当前筛选键 - :param query: 当前搜索词 - :return: 浏览页快照 - """ - active_filter_option = self.resolve_active_filter(filter_key) - active_filter = active_filter_option.key - active_filter_label = active_filter_option.label - search_context = self.resolve_search_context(query) - jobs_payload = NESTED_CLI_SUPPORT.run( - 'job', - 'list', - f'--env={env}', - '--paged', - '--page-size=8', - '--output=json', - parse_json=True, - ).payload - failed_logs_payload = NESTED_CLI_SUPPORT.run( - 'job', - 'logs', - f'--env={env}', - '--paged', - '--page-size=20', - '--status=1', - '--output=json', - parse_json=True, - ).payload - failed_job_names = self.rendering.extract_failed_job_names(failed_logs_payload) - - if not isinstance(jobs_payload, dict) or not jobs_payload.get('ok', False): - return BrowserPageSnapshot( - title='任务', - subtitle=TUI_COPY.build_unavailable_subtitle( - '任务', - SHELL_TEXT_FORMATTER.truncate_text(self.extract_payload_message(jobs_payload), 72), - ), - records=[self.record_builder.build_failure_record(jobs_payload)], - shared_sections=[ - self.section_builder.build_jobs_overview_section( - [], [], failed_job_names, 0, failed_logs_payload, active_filter_label - ), - self.section_builder.build_job_failure_aggregate_section(failed_logs_payload), - ], - filters=list(self.filter_options), - active_filter_key=active_filter, - search=search_context, - ) - - rows = self.extract_page_rows(jobs_payload) - sorted_rows = sorted( - rows, - key=lambda row: ( - 0 - if str(row.get('jobName', '') or '').strip() in failed_job_names - else 1 - if str(row.get('status', '')).strip() == '1' - else 2, - str(row.get('jobName', '') or ''), - ), - ) - filtered_rows = self.row_filter.apply_job_query( - self.row_filter.apply_job_filter(sorted_rows, failed_job_names, active_filter), - query, - ) - records = [self.record_builder.build_job_record(job_row, env) for job_row in filtered_rows[:8]] - if not records: - records = [ - self.build_empty_record( - key='job:none', - subject='任务', - empty_label='任务列表', - has_source_rows=bool(sorted_rows), - filtered_summary='当前筛选条件下没有匹配任务', - empty_summary='当前环境中还没有定时任务', - filtered_empty_value='暂无任务', - empty_empty_value='暂无任务配置', - filtered_detail='当前筛选条件下没有匹配任务', - empty_detail='当前环境中尚未配置定时任务', - ) - ] - paused_count = sum(1 for row in rows if str(row.get('status', '')).strip() == '1') - shared_sections = [ - self.section_builder.build_jobs_overview_section( - rows, - filtered_rows, - failed_job_names, - paused_count, - failed_logs_payload, - active_filter_label, - ), - self.section_builder.build_job_failure_aggregate_section(failed_logs_payload), - ] - return BrowserPageSnapshot( - title='任务', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_jobs_diagnostic_subtitle( - active_filter_label, - len(filtered_rows), - failed_job_names, - paused_count, - ), - records=records, - shared_sections=shared_sections, - filters=list(self.filter_options), - active_filter_key=active_filter, - search=search_context, - ) - - -JOBS_BROWSER_ADAPTER = JobsBrowserAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/models.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/models.py deleted file mode 100644 index b0bca76..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/models.py +++ /dev/null @@ -1,227 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass, field - -from cli.tui.copy import TUI_COPY -from cli.tui.search import PageFilterOption, PageSearchContext -from cli.utils import SHELL_TEXT_FORMATTER - - -@dataclass(frozen=True) -class DetailSectionSnapshot: - """ - TUI 详情页单个分区快照。 - - :param title: 分区标题 - :param status: 分区状态 - :param lines: 分区正文文本行 - """ - - title: str - status: str - lines: list[str] - - -@dataclass(frozen=True) -class DetailPageSnapshot: - """ - TUI 详情页聚合快照。 - - :param title: 页面标题 - :param subtitle: 页面副标题 - :param sections: 页面分区列表 - :param search: 当前页面搜索上下文 - """ - - title: str - subtitle: str - sections: list[DetailSectionSnapshot] - search: PageSearchContext | None = None - - -@dataclass(frozen=True) -class BrowserRecordSnapshot: - """ - TUI 浏览页单条记录快照。 - - :param key: 记录唯一标识 - :param title: 记录标题 - :param status: 记录状态 - :param summary: 记录摘要 - :param metadata_lines: 记录元信息文本行 - :param detail_sections: 记录联动详情分区列表 - """ - - key: str - title: str - status: str - summary: str - metadata_lines: list[str] - detail_sections: list[DetailSectionSnapshot] - detail_loader: Callable[[], list[DetailSectionSnapshot]] | None = None - _cached_detail_sections: tuple[DetailSectionSnapshot, ...] | None = field( - default=None, - init=False, - repr=False, - compare=False, - ) - - def resolve_detail_sections(self) -> list[DetailSectionSnapshot]: - """ - 获取当前记录详情分区,必要时按需加载并缓存。 - - :return: 详情分区列表 - """ - if self.detail_loader is None: - return self.detail_sections - if self._cached_detail_sections is None: - object.__setattr__(self, '_cached_detail_sections', tuple(self.detail_loader())) - return list(self._cached_detail_sections) - - -@dataclass(frozen=True) -class BrowserPageSnapshot: - """ - TUI 浏览页聚合快照。 - - :param title: 页面标题 - :param subtitle: 页面副标题 - :param records: 可浏览记录列表 - :param shared_sections: 全局共享分区列表 - :param filters: 页面可用筛选项 - :param active_filter_key: 当前激活筛选键 - :param search: 当前页面搜索上下文 - """ - - title: str - subtitle: str - records: list[BrowserRecordSnapshot] - shared_sections: list[DetailSectionSnapshot] - filters: list[PageFilterOption] = field(default_factory=list) - active_filter_key: str | None = None - search: PageSearchContext | None = None - - -class TuiAdapterModelRenderService: - """ - TUI 适配层模型渲染服务。 - - 该对象集中封装结果负载摘要提取,以及空态、加载态、失败态文本行 - 的统一构建逻辑,避免这些模块级辅助函数继续散落扩张。 - """ - - @staticmethod - def extract_payload_message(payload: dict[str, object] | None) -> str: - """ - 提取结果负载中的可读消息。 - - :param payload: 标准结果负载 - :return: 摘要消息文本 - """ - if not isinstance(payload, dict): - return '-' - if payload.get('message'): - return SHELL_TEXT_FORMATTER.truncate_text(payload.get('message', '-'), 120) - if payload.get('error'): - return SHELL_TEXT_FORMATTER.truncate_text(payload.get('error', '-'), 120) - return '-' - - @staticmethod - def build_empty_lines( - *, - empty_label: str, - empty_value: str, - detail: str, - suggestion: str = TUI_COPY.build_empty_state_suggestion(), - ) -> list[str]: - """ - 构建统一的空态文本行。 - - :param empty_label: 空态主字段名称 - :param empty_value: 空态主字段值 - :param detail: 空态说明 - :param suggestion: 建议操作 - :return: 空态文本行列表 - """ - return [ - TUI_COPY.build_state_section_title('status'), - f'{empty_label}: {empty_value}', - '', - TUI_COPY.build_state_section_title('detail'), - detail, - '', - TUI_COPY.build_state_section_title('suggestion'), - suggestion, - ] - - @staticmethod - def build_loading_lines( - *, - loading_label: str, - loading_value: str, - detail: str, - suggestion: str = TUI_COPY.build_loading_state_suggestion(), - ) -> list[str]: - """ - 构建统一的加载中文本行。 - - :param loading_label: 加载态主字段名称 - :param loading_value: 加载态主字段值 - :param detail: 加载说明 - :param suggestion: 建议操作 - :return: 加载态文本行列表 - """ - return [ - TUI_COPY.build_state_section_title('status'), - f'{loading_label}: {loading_value}', - '', - TUI_COPY.build_state_section_title('detail'), - detail, - '', - TUI_COPY.build_state_section_title('suggestion'), - suggestion, - ] - - def build_failure_lines( - self, - payload: dict[str, object] | None, - *, - empty_label: str, - empty_value: str, - ) -> list[str]: - """ - 为失败状态构建统一的可读文本行。 - - :param payload: 标准结果负载 - :param empty_label: 主字段标签 - :param empty_value: 主字段兜底值 - :return: 失败状态文本行列表 - """ - lines = [ - TUI_COPY.build_state_section_title('status'), - f'{empty_label}: {empty_value}', - '', - TUI_COPY.build_state_section_title('error'), - ] - if not isinstance(payload, dict): - lines.append('结果消息: -') - lines.extend( - ['', TUI_COPY.build_state_section_title('suggestion'), TUI_COPY.build_failure_state_suggestion()] - ) - return lines - lines.append(f'结果消息: {self.extract_payload_message(payload)}') - detail_lines: list[str] = [] - if payload.get('hint'): - detail_lines.append(f'建议提示: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("hint", "-"), 120)}') - if payload.get('error'): - detail_lines.append(f'错误信息: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("error", "-"), 120)}') - if payload.get('stderr'): - detail_lines.append(f'标准错误: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("stderr", "-"), 120)}') - if payload.get('stdout'): - detail_lines.append(f'标准输出: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("stdout", "-"), 120)}') - if detail_lines: - lines.extend(['', TUI_COPY.build_state_section_title('diagnostic'), *detail_lines]) - lines.extend(['', TUI_COPY.build_state_section_title('suggestion'), TUI_COPY.build_failure_state_suggestion()]) - return lines - - -TUI_ADAPTER_MODEL_RENDERER = TuiAdapterModelRenderService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/ops.py b/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/ops.py deleted file mode 100644 index b5222fc..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/adapters/ops.py +++ /dev/null @@ -1,482 +0,0 @@ -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 OpsDetailSourcePayloads: - """ - 运维详情页原始数据源快照。 - - :param health_payload: `ops health` 结果 - :param ping_db_payload: `ops ping-db` 结果 - :param ping_redis_payload: `ops ping-redis` 结果 - :param deps_payload: `ops deps` 结果 - :param server_payload: `ops server-info` 结果 - """ - - health_payload: dict[str, Any] | None - ping_db_payload: dict[str, Any] | None - ping_redis_payload: dict[str, Any] | None - deps_payload: dict[str, Any] | None - server_payload: dict[str, Any] | None - - -class OpsDetailSnapshotCollector: - """ - 运维详情页数据采集器。 - - 该对象负责拉取运维详情页所需的多路 CLI 原始结果, - 让 `OpsDetailAdapter` 保持详情页编排职责。 - """ - - def collect(self, env: str) -> OpsDetailSourcePayloads: - """ - 采集运维详情页所需原始结果。 - - :param env: 当前运行环境 - :return: 运维详情页原始数据源快照 - """ - return OpsDetailSourcePayloads( - health_payload=NESTED_CLI_SUPPORT.run( - 'ops', - 'health', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - ping_db_payload=NESTED_CLI_SUPPORT.run( - 'ops', - 'ping-db', - f'--env={env}', - '--output=json', - parse_json=True, - ).payload, - ping_redis_payload=NESTED_CLI_SUPPORT.run( - 'ops', - 'ping-redis', - 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 OpsSectionBuilder: - """ - 运维详情分区构建器。 - - 该构建器负责将运维相关 CLI 结果负载转换为 TUI 详情页分区, - 使详情页适配器本体只保留采集与编排职责。 - """ - - @staticmethod - def build_health_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建运维健康检查分区。 - - :param payload: `ops health` 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='不可用' - ), - ) - database = payload.get('database') if isinstance(payload.get('database'), dict) else {} - redis = payload.get('redis') if isinstance(payload.get('redis'), dict) else {} - return DetailSectionSnapshot( - title='健康检查', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 基础连通性', - f'环境: {payload.get("env", "-")}', - f'数据库: {"正常" if database.get("ok", False) else "异常"}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(database.get("message", "-"), 64)}', - f'Redis: {"正常" if redis.get("ok", False) else "异常"}', - f'> {SHELL_TEXT_FORMATTER.truncate_text(redis.get("message", "-"), 64)}', - ], - ) - - @staticmethod - def build_ping_db_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建数据库探活结果分区。 - - :param payload: `ops ping-db` 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='不可用' - ), - ) - return DetailSectionSnapshot( - title='数据库探活', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 探活结果', - f'数据库连接: {"正常" if payload.get("ok", False) else "异常"}', - f'结果摘要: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}', - ], - ) - - @staticmethod - def build_ping_redis_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建 Redis 探活结果分区。 - - :param payload: `ops ping-redis` JSON 负载 - :return: 分区快照 - """ - if not isinstance(payload, dict): - return DetailSectionSnapshot( - title='Redis 探活', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='Redis 探活', empty_value='不可用' - ), - ) - return DetailSectionSnapshot( - title='Redis 探活', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 探活结果', - f'Redis 连接: {"正常" if payload.get("ok", False) else "异常"}', - f'结果摘要: {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}', - ], - ) - - @staticmethod - def build_dependency_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建运维依赖版本分区。 - - :param payload: `ops deps` 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='不可用'), - ) - 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)} 个', - f'> {SHELL_TEXT_FORMATTER.truncate_text(payload.get("message", "-"), 72)}', - '', - '## 关键版本', - ] - for package_name in ('python', 'fastapi', 'sqlalchemy', 'redis', 'typer', 'alembic'): - 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 "-"}') - if missing_required: - lines.extend(['', '## 缺失项']) - lines.extend(f'> {item}' for item in missing_required[:8]) - return DetailSectionSnapshot( - title='依赖版本', - status='ok' if payload.get('ok', False) else 'fail', - lines=lines, - ) - - @staticmethod - def build_server_section(payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建服务器运行概况分区。 - - :param payload: `ops server-info` JSON 负载 - :return: 分区快照 - """ - server = payload.get('server') if isinstance(payload, dict) else None - if not isinstance(server, dict): - return DetailSectionSnapshot( - title='服务器摘要', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='服务器信息', empty_value='不可用' - ), - ) - - 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 {} - py_info = server.get('py') if isinstance(server.get('py'), dict) else {} - return DetailSectionSnapshot( - title='服务器摘要', - status='ok' if payload.get('ok', False) else 'fail', - lines=[ - '## 主机信息', - f'主机名: {sys_info.get("computerName", "-")}', - f'主机 IP: {sys_info.get("computerIp", "-")}', - f'操作系统: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("osName", "-"), 56)}', - f'系统架构: {sys_info.get("osArch", "-")}', - '', - '## 资源负载', - f'CPU 核心: {cpu_info.get("cpuNum", "-")}', - f'CPU 使用率: {cpu_info.get("used", "-")}%', - f'内存总量: {mem_info.get("total", "-")}', - f'内存使用率: {mem_info.get("usage", "-")}%', - '', - '## Python 进程', - f'版本: {py_info.get("version", "-")}', - f'运行时长: {py_info.get("runTime", "-")}', - f'进程内存: {py_info.get("used", "-")} / {py_info.get("total", "-")}', - ], - ) - - def build_disk_section(self, payload: dict[str, Any] | None) -> DetailSectionSnapshot: - """ - 构建服务器磁盘样本分区。 - - :param payload: `ops server-info` JSON 负载 - :return: 分区快照 - """ - server = payload.get('server') if isinstance(payload, dict) else None - if not isinstance(server, dict): - return DetailSectionSnapshot( - title='磁盘样本', - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - payload, empty_label='磁盘样本', empty_value='不可用' - ), - ) - sys_files = server.get('sysFiles') if isinstance(server.get('sysFiles'), list) else [] - lines = TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label='磁盘样本', - empty_value='0 个', - detail='当前服务器信息中没有返回磁盘分区样本', - suggestion=TUI_COPY.build_refresh_page_suggestion('运维', '重新采集服务器信息'), - ) - section_status = 'info' - if sys_files: - section_status = 'ok' - lines = [] - for item in sys_files[:8]: - if not isinstance(item, dict): - continue - lines.extend( - [ - f'## {SHELL_TEXT_FORMATTER.truncate_text(item.get("dirName", "-"), 36)}', - ( - f'> 已用 {item.get("used", "-")} / 总量 {item.get("total", "-")} ' - f'| 使用率 {item.get("usage", "-")}' - ), - f'> 可用 {item.get("free", "-")}', - '', - ] - ) - if lines and lines[-1] == '': - lines.pop() - return DetailSectionSnapshot( - title='磁盘样本', - status=section_status, - lines=lines, - ) - - @staticmethod - def build_prod_check_entry_section() -> DetailSectionSnapshot: - """ - 构建生产巡检向导入口分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title='生产巡检入口', - status='info', - lines=TUI_COPY.build_command_hint_lines( - scenario='准备做生产环境巡检或上线前复核时,应先通过向导统一检查数据库、缓存和运行配置。', - command=TUI_COPY.build_cli_command_hint('wizard', 'prod-check', '--output=text'), - guide='向导会继续询问目标环境并输出预览结果,确认后再执行完整巡检。', - ), - ) - - @staticmethod - def build_overview_section( - health_payload: dict[str, Any] | None, - deps_payload: dict[str, Any] | None, - server_payload: dict[str, Any] | None, - ) -> DetailSectionSnapshot: - """ - 构建运维页总览判断分区。 - - :param health_payload: `ops health` JSON 负载 - :param deps_payload: `ops deps` JSON 负载 - :param server_payload: `ops server-info` JSON 负载 - :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)) - missing_required = deps_payload.get('missingRequired') if isinstance(deps_payload, dict) else None - missing_count = len(missing_required) if isinstance(missing_required, list) else 0 - server = ( - server_payload.get('server') - if isinstance(server_payload, dict) and isinstance(server_payload.get('server'), 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 {} - - status = 'ok' - conclusion = '基础探活与运行依赖正常,可继续查看服务器资源与磁盘样本' - if not health_ok: - status = 'fail' - conclusion = '运维探活存在异常,优先处理数据库或 Redis 连通性问题' - elif not deps_ok or missing_count > 0: - status = 'warn' - conclusion = '运行依赖存在缺口,建议优先补齐核心依赖并确认版本兼容性' - elif not server_ok: - status = 'warn' - conclusion = '服务器信息采集异常,建议重新采集资源与磁盘状态' - - return DetailSectionSnapshot( - title='总览判断', - status=status, - lines=[ - '## 当前结论', - conclusion, - '', - '## 核心指标', - f'探活状态: {"正常" if health_ok else "异常"}', - f'缺失核心依赖: {missing_count} 个', - f'CPU 使用率: {cpu_info.get("used", "-")}%', - f'内存使用率: {mem_info.get("usage", "-")}%', - '', - '## 建议入口', - '优先关注:健康检查 / 依赖版本 / 服务器摘要 / 生产巡检入口', - ], - ) - - def build_sections( - self, - *, - health_payload: dict[str, Any] | None, - ping_db_payload: dict[str, Any] | None, - ping_redis_payload: dict[str, Any] | None, - deps_payload: dict[str, Any] | None, - server_payload: dict[str, Any] | None, - ) -> list[DetailSectionSnapshot]: - """ - 构建运维页全部详情分区。 - - :param health_payload: `ops health` 结果负载 - :param ping_db_payload: `ops ping-db` 结果负载 - :param ping_redis_payload: `ops ping-redis` 结果负载 - :param deps_payload: `ops deps` 结果负载 - :param server_payload: `ops server-info` 结果负载 - :return: 详情分区列表 - """ - return [ - self.build_overview_section(health_payload, deps_payload, server_payload), - self.build_health_section(health_payload), - self.build_ping_db_section(ping_db_payload), - self.build_ping_redis_section(ping_redis_payload), - self.build_dependency_section(deps_payload), - self.build_server_section(server_payload), - self.build_disk_section(server_payload), - self.build_prod_check_entry_section(), - ] - - -class OpsDetailAdapter(BaseDetailAdapter): - """ - 运维详情页适配器。 - - 该适配器负责采集运维相关 CLI 结果,并委托分区构建器组装为 - TUI 详情页快照。 - """ - - def __init__( - self, - section_builder: OpsSectionBuilder | None = None, - snapshot_collector: OpsDetailSnapshotCollector | None = None, - ) -> None: - """ - 初始化运维详情页适配器。 - - :param section_builder: 运维详情分区构建器 - :param snapshot_collector: 运维详情页数据采集器 - :return: None - """ - super().__init__( - page_title='运维', - search_view_key='ops', - default_suggestions=[ - '总览判断', - '健康检查', - '数据库探活', - 'Redis 探活', - '依赖版本', - '服务器摘要', - '磁盘样本', - '生产巡检入口', - ], - ) - self.section_builder = section_builder or OpsSectionBuilder() - self.snapshot_collector = snapshot_collector or OpsDetailSnapshotCollector() - - 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( - health_payload=source_payloads.health_payload, - ping_db_payload=source_payloads.ping_db_payload, - ping_redis_payload=source_payloads.ping_redis_payload, - deps_payload=source_payloads.deps_payload, - server_payload=source_payloads.server_payload, - ) - return DetailPageSnapshot( - title='运维', - subtitle=TUI_DIAGNOSTIC_SERVICE.build_ops_diagnostic_subtitle( - source_payloads.health_payload, - source_payloads.deps_payload, - source_payloads.server_payload, - ), - sections=self.filter_sections(sections, query), - search=self.resolve_search_context(query), - ) - - -OPS_DETAIL_ADAPTER = OpsDetailAdapter() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/app.py b/shierfeng-fastapi-backend/build/lib/cli/tui/app.py deleted file mode 100644 index d95da40..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/app.py +++ /dev/null @@ -1,898 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass -from datetime import datetime - -from textual.app import App -from textual.css.query import NoMatches - -from cli.tui.adapters import ( - TUI_SNAPSHOT_COLLECTOR_REGISTRY, - BrowserPageSnapshot, - DashboardSnapshot, - DetailPageSnapshot, -) -from cli.tui.copy import TUI_COPY -from cli.tui.keymaps import TUI_KEYMAP_REGISTRY -from cli.tui.screens import BrowserScreen, DashboardScreen, DetailScreen -from cli.tui.widgets import NavigationItem, WorkspaceSidebar - -SnapshotCollector = Callable[['ShierfengTuiApp'], BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot] - - -@dataclass(frozen=True) -class TuiViewSpec: - """ - TUI 视图规格定义。 - - :param view_key: 视图标识 - :param include_query: 是否注入搜索词 - :param include_filter: 是否注入筛选键 - """ - - view_key: str - include_query: bool = False - include_filter: bool = False - - -@dataclass(frozen=True) -class TuiViewDefinition: - """ - TUI 视图定义。 - - :param view_key: 视图标识 - :param collector: 页面快照采集器 - """ - - view_key: str - collector: SnapshotCollector - - -@dataclass(frozen=True) -class TuiViewRegistry: - """ - TUI 视图注册表。 - - 该对象集中维护视图打开、刷新与顺序导航所需的元数据, - 避免 `ShierfengTuiApp` 内继续手工维护多份视图映射。 - - :param definitions: 视图定义映射 - :param navigation_items: 导航项列表 - """ - - definitions: dict[str, TuiViewDefinition] - navigation_items: list[NavigationItem] - - @staticmethod - def normalize_view_key(view_key: str) -> str: - """ - 规范化视图标识。 - - :param view_key: 原始视图标识 - :return: 规范化后的视图标识 - """ - return str(view_key).strip().lower() - - def resolve_view_key(self, view_key: str) -> str: - """ - 解析可用视图标识,不存在时回退到 dashboard。 - - :param view_key: 原始视图标识 - :return: 可用视图标识 - """ - normalized_view_key = self.normalize_view_key(view_key) - if normalized_view_key in self.definitions: - return normalized_view_key - return 'dashboard' - - def collect_snapshot( - self, app: 'ShierfengTuiApp', view_key: str - ) -> BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot: - """ - 采集指定视图的页面快照。 - - :param app: 当前 TUI 应用实例 - :param view_key: 目标视图标识 - :return: 页面快照 - """ - resolved_view_key = self.resolve_view_key(view_key) - return self.definitions[resolved_view_key].collector(app) - - def get_navigation_index(self, view_key: str) -> int: - """ - 获取视图在导航列表中的索引。 - - :param view_key: 目标视图标识 - :return: 导航索引 - """ - resolved_view_key = self.resolve_view_key(view_key) - return next( - (index for index, item in enumerate(self.navigation_items) if item.view_key == resolved_view_key), - 0, - ) - - def get_relative_view_key(self, view_key: str, offset: int) -> str: - """ - 获取相对当前视图偏移后的目标视图标识。 - - :param view_key: 当前视图标识 - :param offset: 偏移量 - :return: 目标视图标识 - """ - current_index = self.get_navigation_index(view_key) - target_index = (current_index + offset) % len(self.navigation_items) - return self.navigation_items[target_index].view_key - - -_TUI_VIEW_SPECS: tuple[TuiViewSpec, ...] = ( - TuiViewSpec('dashboard'), - TuiViewSpec('app', include_query=True), - TuiViewSpec('ops', include_query=True), - TuiViewSpec('database', include_query=True), - TuiViewSpec('cache', include_query=True), - TuiViewSpec('jobs', include_query=True, include_filter=True), - TuiViewSpec('gen', include_query=True), - TuiViewSpec('configs', include_query=True, include_filter=True), - TuiViewSpec('crypto', include_query=True), -) - -_TUI_APP_CSS = """ -Screen { - layout: vertical; - background: #04080f; - color: #d8f7ff; - scrollbar-size-horizontal: 1; - scrollbar-size-vertical: 1; - scrollbar-background: #06111b; - scrollbar-background-hover: #0a1723; - scrollbar-background-active: #0d1f2d; - scrollbar-color: #1f6f8d; - scrollbar-color-hover: #38d8ff; - scrollbar-color-active: #6df7ff; -} - -Widget { - scrollbar-size-horizontal: 1; - scrollbar-size-vertical: 1; - scrollbar-background: #06111b; - scrollbar-background-hover: #0a1723; - scrollbar-background-active: #0d1f2d; - scrollbar-color: #1f6f8d; - scrollbar-color-hover: #38d8ff; - scrollbar-color-active: #6df7ff; -} - -#workspace-shell { - height: 1fr; - background: #050c15; -} - -#workspace-main { - padding: 1 2 1 1; - background: #050c15; - border: round #18425d; - margin: 1 1 1 0; - overflow-x: auto; - overflow-y: auto; - scrollbar-size-horizontal: 1; - scrollbar-size-vertical: 1; - scrollbar-background: #06111b; - scrollbar-background-hover: #0a1723; - scrollbar-background-active: #0d1f2d; - scrollbar-color: #1f6f8d; - scrollbar-color-hover: #38d8ff; - scrollbar-color-active: #6df7ff; -} - -#workspace-main:focus { - border: heavy #6df7ff; - background: #071320; -} - -#detail-body { - height: auto; - margin-top: 1; - border: double #18425d; - background: #06111b; - padding: 1; - overflow-x: auto; - overflow-y: auto; -} - -#browser-body { - height: auto; - margin-top: 1; - border: double #18425d; - background: #06111b; - padding: 1; - overflow-x: auto; - overflow-y: auto; -} - -#browser-filter-bar { - border: round #163c57; - padding: 1 2; - margin-bottom: 1; - background: #07131f; - color: #d9fbff; -} - -#browser-detail-pane { - width: 1fr; - min-width: 48; - border: round #163c57; - padding: 1; - background: #07131f; - overflow-x: auto; - overflow-y: auto; -} - -#browser-detail-pane.is-ok { - border: round #22c983; -} - -#browser-detail-pane.is-fail { - border: round #ff6b7a; - background: #190d12; -} - -#browser-detail-pane.is-warn { - border: round #f2b84b; - background: #171108; -} - -#browser-detail-pane.is-info { - border: round #38d8ff; -} - -#browser-section-body { - height: auto; - border-top: solid #163c57; - padding-top: 1; - background: #06111b; - overflow-x: auto; - overflow-y: auto; -} - -#dashboard-metrics { - grid-size: 4; - grid-gutter: 1 2; - grid-columns: 1fr 1fr 1fr 1fr; - height: auto; - margin-top: 1; - margin-bottom: 1; - padding: 1 0; -} - -#dashboard-grid, #detail-grid { - grid-size: 2; - grid-gutter: 1 2; - grid-columns: 1fr 1fr; - height: auto; - margin-top: 1; -} - -#detail-grid { - margin-bottom: 1; -} - -Footer { - background: #060d16; - color: #b6efff; - border-top: heavy #163c57; -} - -FooterKey { - background: #0b2130; - color: #ecfcff; -} -""" - - -@dataclass(frozen=True) -class TuiBindingSpec: - """ - TUI 快捷键绑定定义。 - - :param key: 快捷键 - :param action: 动作名称 - :param label: 展示标签 - """ - - key: str - action: str - label: str - - -class TuiBindingBuilder: - """ - TUI 快捷键绑定构建器。 - - 该对象负责统一装配应用级固定绑定和视图跳转绑定,避免 `ShierfengTuiApp` - 继续手工维护重复的绑定元组列表。 - """ - - def build_base_bindings(self) -> list[TuiBindingSpec]: - """ - 构建应用级固定绑定。 - - :return: 绑定定义列表 - """ - return [ - TuiBindingSpec('q', 'quit', TUI_COPY.build_app_binding_label('quit')), - TuiBindingSpec('s', 'focus_sidebar', TUI_COPY.build_app_binding_label('sidebar')), - TuiBindingSpec('[', 'show_previous_view', TUI_COPY.build_app_binding_label('previous')), - TuiBindingSpec(']', 'show_next_view', TUI_COPY.build_app_binding_label('next')), - TuiBindingSpec('r', 'refresh_current_view', TUI_COPY.build_app_binding_label('refresh')), - ] - - def build_navigation_bindings(self) -> list[TuiBindingSpec]: - """ - 构建页面导航绑定。 - - :return: 绑定定义列表 - """ - return [ - TuiBindingSpec( - TUI_KEYMAP_REGISTRY.get_navigation_shortcut(spec.view_key), - f'show_{spec.view_key}', - TUI_COPY.render_view_label(spec.view_key), - ) - for spec in _TUI_VIEW_SPECS - ] - - def build(self) -> list[tuple[str, str, str]]: - """ - 构建 Textual 所需的绑定元组列表。 - - :return: 绑定元组列表 - """ - return [ - (binding.key, binding.action, binding.label) - for binding in [*self.build_base_bindings(), *self.build_navigation_bindings()] - ] - - -TUI_APP_BINDINGS = TuiBindingBuilder().build() - - -@dataclass(frozen=True) -class TuiViewRegistryBuilder: - """ - TUI 视图注册表构建器。 - - 该对象负责根据统一规格构建导航项和快照采集定义,避免模块内 - 继续维护重复的 `lambda collect(...)` 配置块。 - """ - - def build_navigation_items(self) -> list[NavigationItem]: - """ - 构建工作台导航项列表。 - - :return: 导航项列表 - """ - return [ - NavigationItem( - view_key, - TUI_COPY.render_view_label(view_key), - shortcut, - TUI_COPY.render_navigation_description(view_key), - ) - for view_key, shortcut in TUI_KEYMAP_REGISTRY.navigation_shortcuts.items() - ] - - def build_collector(self, spec: TuiViewSpec) -> SnapshotCollector: - """ - 根据视图规格构建页面快照采集器。 - - :param spec: 视图规格 - :return: 页面快照采集器 - """ - - def collect(app: 'ShierfengTuiApp') -> BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot: - collect_kwargs: dict[str, str] = {} - if spec.include_filter: - collect_kwargs['filter_key'] = app.get_browser_filter(spec.view_key) - if spec.include_query: - collect_kwargs['query'] = app.get_browser_query(spec.view_key) - return TUI_SNAPSHOT_COLLECTOR_REGISTRY.collect(spec.view_key, app.env, **collect_kwargs) - - return collect - - def build_definitions(self) -> dict[str, TuiViewDefinition]: - """ - 构建视图定义映射。 - - :return: 视图定义映射 - """ - return {spec.view_key: TuiViewDefinition(spec.view_key, self.build_collector(spec)) for spec in _TUI_VIEW_SPECS} - - def build(self) -> TuiViewRegistry: - """ - 构建 TUI 视图注册表。 - - :return: 视图注册表 - """ - return TuiViewRegistry( - definitions=self.build_definitions(), - navigation_items=self.build_navigation_items(), - ) - - -@dataclass -class TuiViewStateStore: - """ - TUI 视图状态存储。 - - 该对象集中维护页面级动作反馈、浏览页筛选键和搜索词,避免 - `ShierfengTuiApp` 本体继续直接操作多份同类字典状态。 - - :param action_feedback_by_view: 动作反馈映射 - :param browser_filter_by_view: 浏览页筛选映射 - :param browser_query_by_view: 浏览页搜索词映射 - """ - - action_feedback_by_view: dict[str, list[str]] - browser_filter_by_view: dict[str, str] - browser_query_by_view: dict[str, str] - - @classmethod - def create_default(cls) -> 'TuiViewStateStore': - """ - 创建默认视图状态存储。 - - :return: 视图状态存储 - """ - return cls( - action_feedback_by_view={}, - browser_filter_by_view={'jobs': 'all', 'configs': 'all'}, - browser_query_by_view={}, - ) - - def remember_action_feedback(self, view_key: str, lines: list[str]) -> None: - """ - 记录指定页面最近一次动作反馈。 - - :param view_key: 页面视图标识 - :param lines: 反馈文本行 - :return: None - """ - self.action_feedback_by_view[view_key] = list(lines) - - def get_action_feedback_lines(self, view_key: str) -> list[str]: - """ - 读取指定页面最近一次动作反馈。 - - :param view_key: 页面视图标识 - :return: 反馈文本行 - """ - return list(self.action_feedback_by_view.get(view_key, [])) - - def remember_browser_filter(self, view_key: str, filter_key: str) -> None: - """ - 记录指定浏览页当前筛选键。 - - :param view_key: 页面视图标识 - :param filter_key: 当前筛选键 - :return: None - """ - self.browser_filter_by_view[view_key] = filter_key - - def get_browser_filter(self, view_key: str, default: str = 'all') -> str: - """ - 读取指定浏览页当前筛选键。 - - :param view_key: 页面视图标识 - :param default: 默认筛选键 - :return: 当前筛选键 - """ - return str(self.browser_filter_by_view.get(view_key, default) or default) - - def remember_browser_query(self, view_key: str, query: str) -> None: - """ - 记录指定浏览页当前搜索词。 - - :param view_key: 页面视图标识 - :param query: 当前搜索词 - :return: None - """ - normalized_query = str(query or '').strip() - if normalized_query: - self.browser_query_by_view[view_key] = normalized_query - return - self.browser_query_by_view.pop(view_key, None) - - def get_browser_query(self, view_key: str, default: str = '') -> str: - """ - 读取指定浏览页当前搜索词。 - - :param view_key: 页面视图标识 - :param default: 默认搜索词 - :return: 当前搜索词 - """ - return str(self.browser_query_by_view.get(view_key, default) or default) - - -TUI_VIEW_REGISTRY = TuiViewRegistryBuilder().build() -NAVIGATION_ITEMS = TUI_VIEW_REGISTRY.navigation_items - - -@dataclass(frozen=True) -class TuiScreenFactory: - """ - TUI 工作台页面工厂。 - - 该对象负责把页面快照转换为具体 screen 实例,避免 `ShierfengTuiApp` - 同时承担状态协调和 screen 构建职责。 - """ - - @staticmethod - def build_refresh_timestamp() -> str: - """ - 构建当前页面刷新时间文本。 - - :return: 格式化后的刷新时间 - """ - return datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - def build( - self, - *, - snapshot: BrowserPageSnapshot | DashboardSnapshot | DetailPageSnapshot, - env: str, - active_view: str, - navigation_items: list[NavigationItem], - action_feedback_lines: list[str], - ) -> BrowserScreen | DashboardScreen | DetailScreen: - """ - 根据快照类型构建对应的工作台页面。 - - :param snapshot: 页面快照 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param navigation_items: 导航项列表 - :param action_feedback_lines: 当前视图动作反馈 - :return: 对应的页面对象 - """ - refreshed_at = self.build_refresh_timestamp() - if isinstance(snapshot, DashboardSnapshot): - return DashboardScreen( - snapshot, - env=env, - active_view=active_view, - navigation_items=navigation_items, - refreshed_at=refreshed_at, - ) - if isinstance(snapshot, BrowserPageSnapshot): - return BrowserScreen( - snapshot, - env=env, - active_view=active_view, - navigation_items=navigation_items, - refreshed_at=refreshed_at, - action_feedback_lines=action_feedback_lines, - ) - return DetailScreen( - snapshot, - env=env, - active_view=active_view, - navigation_items=navigation_items, - refreshed_at=refreshed_at, - ) - - -@dataclass -class TuiScreenNavigator: - """ - TUI 工作台页面导航器。 - - 该对象负责处理首屏展示与后续切屏差异,让应用壳更专注于状态切换。 - - :param app: 当前 TUI 应用实例 - :param initialized: 工作台屏幕是否已初始化 - """ - - app: 'ShierfengTuiApp' - initialized: bool = False - - def show(self, screen: BrowserScreen | DashboardScreen | DetailScreen) -> None: - """ - 显示工作台页面,并兼容首屏初始化与后续切屏。 - - Textual 在默认根屏阶段直接调用 `switch_screen()` 会触发内部回调栈异常, - 因此首次展示页面需要使用 `push_screen()` 建立活动屏,后续再改用 - `switch_screen()` 做同层页面替换。 - - :param screen: 待显示的页面对象 - :return: None - """ - if not self.initialized: - self.app.push_screen(screen) - self.initialized = True - return - self.app.switch_screen(screen) - - -@dataclass(frozen=True) -class TuiViewOpeningCoordinator: - """ - TUI 视图打开协调器。 - - 该对象负责串联视图解析、快照采集、screen 构建与切屏展示, - 让 `ShierfengTuiApp` 更专注于状态暴露与 Textual action 桥接。 - - :param view_registry: 视图注册表 - :param screen_factory: 工作台页面工厂 - :param navigation_items: 导航项列表 - """ - - view_registry: TuiViewRegistry - screen_factory: TuiScreenFactory - navigation_items: list[NavigationItem] - - def show(self, app: 'ShierfengTuiApp', view_key: str) -> None: - """ - 切换并展示指定视图。 - - :param app: 当前 TUI 应用实例 - :param view_key: 目标视图标识 - :return: None - """ - resolved_view_key = self.view_registry.resolve_view_key(view_key) - app.current_view = resolved_view_key - snapshot = self.view_registry.collect_snapshot(app, resolved_view_key) - screen = self.screen_factory.build( - snapshot=snapshot, - env=app.env, - active_view=app.current_view, - navigation_items=self.navigation_items, - action_feedback_lines=app.get_action_feedback_lines(app.current_view), - ) - app.screen_navigator.show(screen) - - -class ShierfengTuiApp(App[None]): - """ - ShiErFeng CLI Textual 应用。 - - :param env: 当前运行环境 - """ - - CSS = _TUI_APP_CSS - BINDINGS = TUI_APP_BINDINGS - - def __init__(self, env: str) -> None: - """ - 初始化 TUI 应用实例。 - - :param env: 当前运行环境 - :return: None - """ - self.env = env - self.current_view = 'dashboard' - self.view_state_store = TuiViewStateStore.create_default() - self.screen_factory = TuiScreenFactory() - self.screen_navigator = TuiScreenNavigator(self) - self.view_opening_coordinator = TuiViewOpeningCoordinator( - view_registry=TUI_VIEW_REGISTRY, - screen_factory=self.screen_factory, - navigation_items=NAVIGATION_ITEMS, - ) - super().__init__() - - def show_view(self, view_key: str) -> None: - """ - 切换并展示指定视图。 - - :param view_key: 目标视图标识 - :return: None - """ - self.view_opening_coordinator.show(self, view_key) - - def remember_action_feedback(self, view_key: str, lines: list[str]) -> None: - """ - 记录指定页面最近一次动作反馈。 - - :param view_key: 页面视图标识 - :param lines: 反馈文本行 - :return: None - """ - self.view_state_store.remember_action_feedback(view_key, lines) - - def get_action_feedback_lines(self, view_key: str) -> list[str]: - """ - 读取指定页面最近一次动作反馈。 - - :param view_key: 页面视图标识 - :return: 反馈文本行 - """ - return self.view_state_store.get_action_feedback_lines(view_key) - - def remember_browser_filter(self, view_key: str, filter_key: str) -> None: - """ - 记录指定浏览页当前筛选键。 - - :param view_key: 页面视图标识 - :param filter_key: 当前筛选键 - :return: None - """ - self.view_state_store.remember_browser_filter(view_key, filter_key) - - def get_browser_filter(self, view_key: str, default: str = 'all') -> str: - """ - 读取指定浏览页当前筛选键。 - - :param view_key: 页面视图标识 - :param default: 默认筛选键 - :return: 当前筛选键 - """ - return self.view_state_store.get_browser_filter(view_key, default) - - def remember_browser_query(self, view_key: str, query: str) -> None: - """ - 记录指定浏览页当前搜索词。 - - :param view_key: 页面视图标识 - :param query: 当前搜索词 - :return: None - """ - self.view_state_store.remember_browser_query(view_key, query) - - def get_browser_query(self, view_key: str, default: str = '') -> str: - """ - 读取指定浏览页当前搜索词。 - - :param view_key: 页面视图标识 - :param default: 默认搜索词 - :return: 当前搜索词 - """ - return self.view_state_store.get_browser_query(view_key, default) - - def on_mount(self) -> None: - """ - 在应用启动时载入首页。 - - :return: None - """ - self.show_view('dashboard') - - def open_view(self, view_key: str) -> None: - """ - 按视图标识打开对应页面。 - - :param view_key: 视图标识 - :return: None - """ - self.show_view(view_key) - - def action_focus_sidebar(self) -> None: - """ - 将焦点切换到左侧导航栏。 - - :return: None - """ - try: - self.screen.query_one(WorkspaceSidebar).focus() - except NoMatches: - return - - def action_show_previous_view(self) -> None: - """ - 切换到前一个工作台视图。 - - :return: None - """ - self.open_view(TUI_VIEW_REGISTRY.get_relative_view_key(self.current_view, -1)) - - def action_show_next_view(self) -> None: - """ - 切换到后一个工作台视图。 - - :return: None - """ - self.open_view(TUI_VIEW_REGISTRY.get_relative_view_key(self.current_view, 1)) - - def action_refresh_current_view(self) -> None: - """ - 刷新当前视图数据。 - - :return: None - """ - self.show_view(self.current_view) - - def action_show_dashboard(self) -> None: - """ - 切换到首页视图。 - - :return: None - """ - self.open_view('dashboard') - - def action_show_app(self) -> None: - """ - 切换到应用详情视图。 - - :return: None - """ - self.open_view('app') - - def action_show_ops(self) -> None: - """ - 切换到运维详情视图。 - - :return: None - """ - self.open_view('ops') - - def action_show_database(self) -> None: - """ - 切换到数据库详情视图。 - - :return: None - """ - self.open_view('database') - - def action_show_cache(self) -> None: - """ - 切换到缓存浏览视图。 - - :return: None - """ - self.open_view('cache') - - def action_show_jobs(self) -> None: - """ - 切换到任务浏览视图。 - - :return: None - """ - self.open_view('jobs') - - def action_show_gen(self) -> None: - """ - 切换到代码生成浏览视图。 - - :return: None - """ - self.open_view('gen') - - def action_show_configs(self) -> None: - """ - 切换到配置浏览视图。 - - :return: None - """ - self.open_view('configs') - - def action_show_crypto(self) -> None: - """ - 切换到传输加密详情视图。 - - :return: None - """ - self.open_view('crypto') - - -@dataclass(frozen=True) -class TuiAppRunner: - """ - TUI 应用启动器。 - - 该对象负责按环境创建并运行 Textual 应用实例,使命令入口不再依赖 - 模块级启动函数。 - - :param app_factory: TUI 应用工厂 - """ - - app_factory: Callable[[str], ShierfengTuiApp] - - def run(self, env: str) -> None: - """ - 启动 TUI 应用。 - - :param env: 当前运行环境 - :return: None - """ - self.app_factory(env).run() - - -TUI_APP_RUNNER = TuiAppRunner(ShierfengTuiApp) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/capabilities.py b/shierfeng-fastapi-backend/build/lib/cli/tui/capabilities.py deleted file mode 100644 index 95f7b12..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/capabilities.py +++ /dev/null @@ -1,209 +0,0 @@ -from dataclasses import dataclass -from typing import Literal - -from cli.tui.copy import TUI_COPY - -CapabilityKind = Literal['read_only', 'preview', 'low_risk_action', 'wizard_entry', 'command_hint'] -CapabilityScope = Literal['browser', 'detail'] - - -@dataclass(frozen=True) -class TuiCapability: - """ - TUI 页面能力定义。 - - :param kind: 能力类型 - :param slot: 绑定的动作槽位 - :param key: 绑定的快捷键 - :param label: 展示标签 - :param hint_label: 用于顶部提示的简洁标签 - :param scope: 能力所在页面范围 - """ - - kind: CapabilityKind - slot: str - key: str - label: str - hint_label: str - scope: CapabilityScope - - -@dataclass(frozen=True) -class TuiCapabilitySpec: - """ - TUI 页面能力规格定义。 - - :param capability_key: 能力标识 - :param kind: 能力类型 - :param slot: 绑定动作槽位 - :param key: 绑定快捷键 - """ - - capability_key: str - kind: CapabilityKind - slot: str - key: str - - -_BROWSER_CAPABILITY_SPECS: dict[str, tuple[TuiCapabilitySpec, ...]] = { - 'jobs': ( - TuiCapabilitySpec('job_run_once', 'low_risk_action', 'primary', 'X'), - TuiCapabilitySpec('job_toggle', 'low_risk_action', 'secondary', 'Z'), - TuiCapabilitySpec('job_sync', 'preview', 'global', 'Y'), - ), - 'configs': (TuiCapabilitySpec('config_sync', 'low_risk_action', 'global', 'Y'),), - 'cache': ( - TuiCapabilitySpec('cache_clear_wizard', 'wizard_entry', 'global', 'Y'), - TuiCapabilitySpec('cache_warmup', 'low_risk_action', 'utility', 'W'), - ), - 'gen': ( - TuiCapabilitySpec('gen_export_wizard', 'wizard_entry', 'primary', 'X'), - TuiCapabilitySpec('gen_import_wizard', 'wizard_entry', 'secondary', 'Z'), - TuiCapabilitySpec('gen_export_dry_run', 'preview', 'global', 'Y'), - TuiCapabilitySpec('gen_sync_db', 'low_risk_action', 'utility', 'W'), - ), -} - -_DETAIL_CAPABILITY_SPECS: dict[str, tuple[TuiCapabilitySpec, ...]] = { - 'app': ( - TuiCapabilitySpec('app_run', 'wizard_entry', 'primary', 'X'), - TuiCapabilitySpec('app_run_wizard', 'wizard_entry', 'global', 'Y'), - TuiCapabilitySpec('completion_install', 'command_hint', 'utility', 'W'), - ), - 'database': ( - TuiCapabilitySpec('db_upgrade_wizard', 'wizard_entry', 'global', 'Y'), - TuiCapabilitySpec('db_init_dry_run', 'preview', 'utility', 'W'), - ), - 'ops': ( - TuiCapabilitySpec('ops_ping_db', 'low_risk_action', 'primary', 'X'), - TuiCapabilitySpec('ops_ping_redis', 'low_risk_action', 'secondary', 'Z'), - TuiCapabilitySpec('prod_check_wizard', 'wizard_entry', 'global', 'Y'), - ), - 'crypto': ( - TuiCapabilitySpec('crypto_keygen', 'wizard_entry', 'primary', 'X'), - TuiCapabilitySpec('crypto_rotate_dry_run', 'preview', 'global', 'Y'), - ), -} - - -@dataclass(frozen=True) -class TuiCapabilityRegistry: - """ - TUI 页面能力注册表。 - - 该对象集中维护浏览页与详情页的能力描述,避免模块级散落 - 的字典常量在后续扩展时继续增长。 - - :param browser_capabilities: 浏览页能力映射 - :param detail_capabilities: 详情页能力映射 - """ - - browser_capabilities: dict[str, tuple[TuiCapability, ...]] - detail_capabilities: dict[str, tuple[TuiCapability, ...]] - - @staticmethod - def normalize_view_key(view_key: str) -> str: - """ - 规范化页面标识。 - - :param view_key: 原始页面标识 - :return: 规范化后的页面标识 - """ - return str(view_key).strip().lower() - - def get_browser_capabilities(self, view_key: str) -> tuple[TuiCapability, ...]: - """ - 获取浏览页支持的能力列表。 - - :param view_key: 页面视图标识 - :return: 能力列表 - """ - return self.browser_capabilities.get(self.normalize_view_key(view_key), ()) - - def get_detail_capabilities(self, view_key: str) -> tuple[TuiCapability, ...]: - """ - 获取详情页支持的能力列表。 - - :param view_key: 页面视图标识 - :return: 能力列表 - """ - return self.detail_capabilities.get(self.normalize_view_key(view_key), ()) - - -@dataclass(frozen=True) -class TuiCapabilityRegistryBuilder: - """ - TUI 页面能力注册表构建器。 - - 该对象负责通过统一文案服务构建页面能力定义,避免模块内继续 - 直接堆叠硬编码的标题和提示标签。 - """ - - def build_capability( - self, - *, - capability_key: str, - kind: CapabilityKind, - slot: str, - key: str, - scope: CapabilityScope, - ) -> TuiCapability: - """ - 构建单条能力定义。 - - :param capability_key: 能力标识 - :param kind: 能力类型 - :param slot: 绑定动作槽位 - :param key: 绑定快捷键 - :param scope: 页面范围 - :return: 能力定义 - """ - return TuiCapability( - kind=kind, - slot=slot, - key=key, - label=TUI_COPY.build_capability_label(capability_key), - hint_label=TUI_COPY.build_capability_hint_label(capability_key), - scope=scope, - ) - - def build_scope_capabilities( - self, - specs_by_view: dict[str, tuple[TuiCapabilitySpec, ...]], - *, - scope: CapabilityScope, - ) -> dict[str, tuple[TuiCapability, ...]]: - """ - 根据规格映射构建指定页面范围的能力注册表。 - - :param specs_by_view: 页面能力规格映射 - :param scope: 页面范围 - :return: 构建后的能力映射 - """ - return { - view_key: tuple( - self.build_capability( - capability_key=spec.capability_key, - kind=spec.kind, - slot=spec.slot, - key=spec.key, - scope=scope, - ) - for spec in specs - ) - for view_key, specs in specs_by_view.items() - } - - def build(self) -> TuiCapabilityRegistry: - """ - 构建页面能力注册表。 - - :return: 能力注册表 - """ - return TuiCapabilityRegistry( - browser_capabilities=self.build_scope_capabilities(_BROWSER_CAPABILITY_SPECS, scope='browser'), - detail_capabilities=self.build_scope_capabilities(_DETAIL_CAPABILITY_SPECS, scope='detail'), - ) - - -TUI_CAPABILITY_REGISTRY = TuiCapabilityRegistryBuilder().build() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/commands.py b/shierfeng-fastapi-backend/build/lib/cli/tui/commands.py deleted file mode 100644 index e6e62e6..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/commands.py +++ /dev/null @@ -1,145 +0,0 @@ -from dataclasses import dataclass -from importlib import import_module -from types import ModuleType - -import typer - -from cli.context import EnvOption -from cli.core import DEFAULT_CORE_SERVICES, CliContextFactory, CliExecutionService -from cli.exit_codes import DEPENDENCY_ERROR -from cli.output import CommandResult -from cli.tui.copy import TUI_COPY - - -@dataclass(frozen=True) -class TuiDependencyResultFactory: - """ - TUI 依赖结果构建器。 - - 该对象负责统一构建 Textual/Rich 缺失时的降级结果, - 避免命令注册层继续直接拼接错误负载。 - """ - - def build_missing_dependency_result(self) -> CommandResult: - """ - 构建 TUI 可选依赖缺失时的提示结果。 - - :return: 命令结果对象 - """ - return CommandResult( - data={ - 'ok': False, - 'message': TUI_COPY.build_missing_dependency_message(), - 'hint': TUI_COPY.build_missing_dependency_hint(), - }, - exit_code=DEPENDENCY_ERROR, - ) - - -@dataclass(frozen=True) -class TuiAppModuleLoader: - """ - TUI 应用模块加载器。 - - 该对象负责识别缺失的可选依赖,并在命令执行阶段按需导入 - `cli.tui.app` 模块。 - """ - - def is_missing_dependency_error(self, exc: ModuleNotFoundError) -> bool: - """ - 判断模块导入异常是否由 TUI 可选依赖缺失触发。 - - :param exc: 模块导入异常 - :return: 是否为 TUI 依赖缺失 - """ - missing_module_name = (exc.name or '').split('.', 1)[0] - return missing_module_name in {'textual', 'rich'} - - def load(self) -> ModuleType: - """ - 惰性导入 TUI 应用模块。 - - :return: `cli.tui.app` 模块对象 - """ - return import_module('cli.tui.app') - - -@dataclass(frozen=True) -class TuiCommandRegistration: - """ - TUI 命令注册器。 - - :param context_factory: CLI 上下文工厂 - :param execution_service: CLI 执行服务 - :param dependency_result_factory: TUI 缺失依赖结果构建器 - :param module_loader: TUI 应用模块加载器 - """ - - context_factory: CliContextFactory - execution_service: CliExecutionService - dependency_result_factory: TuiDependencyResultFactory - module_loader: TuiAppModuleLoader - - def handle_tui_command(self, env: str) -> None: - """ - 执行 `shierfeng tui` 命令。 - - :param env: 当前命令运行环境 - :return: None - """ - ctx = self.context_factory.build_readonly(env, 'text') - try: - tui_app_module = self.module_loader.load() - except ModuleNotFoundError as exc: - if self.module_loader.is_missing_dependency_error(exc): - self.execution_service.complete_result( - ctx, - self.dependency_result_factory.build_missing_dependency_result(), - ) - return - raise - tui_app_module.TUI_APP_RUNNER.run(env) - - def build(self) -> typer.Typer: - """ - 构建 `tui` 子应用。 - - :return: `tui` 子应用 - """ - app = typer.Typer( - help=TUI_COPY.build_tui_command_help(), - no_args_is_help=False, - invoke_without_command=True, - context_settings={'help_option_names': ['-h', '--help']}, - ) - - @app.callback() - def tui( - env: EnvOption = 'dev', - ) -> None: - """ - 进入只读巡检工作台。 - - :param env: 当前命令运行环境 - :return: None - """ - self.handle_tui_command(env) - - return app - - def register(self, root_cli: typer.Typer) -> None: - """ - 向根 Typer 应用注册 `tui` 命令。 - - :param root_cli: 根 Typer 应用 - :return: None - """ - root_cli.add_typer(self.build(), name='tui') - - -TUI_COMMAND_REGISTRATION = TuiCommandRegistration( - context_factory=DEFAULT_CORE_SERVICES.context_factory, - execution_service=DEFAULT_CORE_SERVICES.execution_service, - dependency_result_factory=TuiDependencyResultFactory(), - module_loader=TuiAppModuleLoader(), -) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/copy/__init__.py deleted file mode 100644 index 4c4e3de..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from cli.tui.copy.actions import TuiActionCopyMixin -from cli.tui.copy.navigation import TuiNavigationCopyMixin -from cli.tui.copy.workspace import TuiWorkspaceCopyMixin - - -class TuiCopyService(TuiNavigationCopyMixin, TuiActionCopyMixin, TuiWorkspaceCopyMixin): - """ - TUI 文案构造服务。 - - 该服务通过组合导航、动作和工作区文案混入对象,统一对外提供 - TUI 所需的文案构造能力,避免入口模块继续堆积实现细节。 - """ - - -TUI_COPY = TuiCopyService() - -__all__ = [ - 'TUI_COPY', - 'TuiActionCopyMixin', - 'TuiCopyService', - 'TuiNavigationCopyMixin', - 'TuiWorkspaceCopyMixin', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/actions.py b/shierfeng-fastapi-backend/build/lib/cli/tui/copy/actions.py deleted file mode 100644 index 6a45126..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/actions.py +++ /dev/null @@ -1,298 +0,0 @@ -from cli.tui.copy.fragments import ( - ACTION_CONSEQUENCE_TEXTS, - ACTION_FEEDBACK_COPY, - ACTION_HINT_COPY, - ACTION_LABELS, - ACTION_NOTIFICATION_COPY, - ACTION_PREVIEW_FIELD_LABELS, - ACTION_PREVIEW_TITLES, - ACTION_PURPOSE_LABELS, - ACTION_RESULT_FIELD_LABELS, - ACTION_SCOPE_LABELS, - CAPABILITY_HINT_LABELS, - CAPABILITY_LABELS, - COMMAND_HINT_TITLES, - GENERAL_COPY, -) -from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER - - -class TuiActionCopyMixin: - """ - TUI 动作与能力文案混入。 - """ - - @staticmethod - def build_action_confirm_hint() -> str: - """ - 构建动作确认弹窗底部提示文案。 - - :return: 确认提示文案 - """ - return GENERAL_COPY['action_confirm_hint'] - - @staticmethod - def build_action_confirm_cancel_label() -> str: - """ - 构建动作确认弹窗取消按钮文案。 - - :return: 取消按钮文案 - """ - return GENERAL_COPY['action_confirm_cancel_label'] - - @staticmethod - def build_action_label(action_key: str) -> str: - """ - 根据动作标识构建动作标题文案。 - - :param action_key: 动作标识 - :return: 动作标题文案 - """ - normalized_key = str(action_key).strip().lower() - return ACTION_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_scope_label(action_key: str) -> str: - """ - 根据动作标识构建作用范围文案。 - - :param action_key: 动作标识 - :return: 作用范围文案 - """ - normalized_key = str(action_key).strip().lower() - return ACTION_SCOPE_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_purpose_label(action_key: str) -> str: - """ - 根据动作标识构建用途文案。 - - :param action_key: 动作标识 - :return: 用途文案 - """ - normalized_key = str(action_key).strip().lower() - return ACTION_PURPOSE_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_preview_title(title_key: str) -> str: - """ - 根据预览区块标识构建标题文案。 - - :param title_key: 预览区块标识 - :return: 标题文案 - """ - normalized_key = str(title_key).strip().lower() - return ACTION_PREVIEW_TITLES.get(normalized_key, normalized_key) - - @staticmethod - def build_action_preview_field_label(field_key: str) -> str: - """ - 根据预览字段标识构建字段文案。 - - :param field_key: 预览字段标识 - :return: 字段文案 - """ - normalized_key = str(field_key).strip().lower() - return ACTION_PREVIEW_FIELD_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_result_field_label(field_key: str) -> str: - """ - 根据结果字段标识构建字段文案。 - - :param field_key: 结果字段标识 - :return: 字段文案 - """ - normalized_key = str(field_key).strip().lower() - return ACTION_RESULT_FIELD_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_notification_title() -> str: - """ - 构建动作通知标题文案。 - - :return: 通知标题文案 - """ - return ACTION_NOTIFICATION_COPY['title'] - - @staticmethod - def build_action_unavailable_message() -> str: - """ - 构建无可执行动作时的提示文案。 - - :return: 提示文案 - """ - return ACTION_NOTIFICATION_COPY['unavailable_message'] - - @staticmethod - def build_action_empty_line() -> str: - """ - 构建无可执行动作时的空态占位文案。 - - :return: 空态占位文案 - """ - return ACTION_NOTIFICATION_COPY['empty_line'] - - @staticmethod - def build_action_consequence_text(consequence_key: str) -> str: - """ - 根据后果说明标识构建说明文案。 - - :param consequence_key: 后果说明标识 - :return: 后果说明文案 - """ - normalized_key = str(consequence_key).strip().lower() - return ACTION_CONSEQUENCE_TEXTS.get(normalized_key, normalized_key) - - @staticmethod - def build_capability_label(capability_key: str) -> str: - """ - 根据能力标识构建展示标签文案。 - - :param capability_key: 能力标识 - :return: 展示标签文案 - """ - normalized_key = str(capability_key).strip().lower() - return CAPABILITY_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_capability_hint_label(capability_key: str) -> str: - """ - 根据能力标识构建顶部提示短标签文案。 - - :param capability_key: 能力标识 - :return: 短标签文案 - """ - normalized_key = str(capability_key).strip().lower() - return CAPABILITY_HINT_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_action_running_message(label: str) -> str: - """ - 构建动作执行中的提示文案。 - - :param label: 动作标题 - :return: 提示文案 - """ - return f'正在执行:{label}' - - @staticmethod - def build_browser_action_hint_text(view_key: str, interaction_hint: str) -> str: - """ - 构建浏览页动作提示文本。 - - :param view_key: 当前页面视图标识 - :param interaction_hint: 通用浏览提示 - :return: 动作提示文本 - """ - normalized_view = str(view_key).strip().lower() - template = ACTION_HINT_COPY['browser_templates'].get(normalized_view, ACTION_HINT_COPY['browser_default']) - return template.format(interaction_hint=interaction_hint) - - @staticmethod - def build_detail_action_hint_text(view_key: str, interaction_hint: str) -> str: - """ - 构建详情页动作提示文本。 - - :param view_key: 当前页面视图标识 - :param interaction_hint: 通用浏览提示 - :return: 动作提示文本 - """ - normalized_view = str(view_key).strip().lower() - template = ACTION_HINT_COPY['detail_templates'].get(normalized_view, ACTION_HINT_COPY['detail_default']) - return template.format(interaction_hint=interaction_hint) - - @staticmethod - def build_action_line(key: str, label: str) -> str: - """ - 构建单条动作快捷键信息。 - - :param key: 快捷键 - :param label: 动作标题 - :return: 文本文案 - """ - return f'动作键 [{key}] · {label}' - - @staticmethod - def build_capability_hint_text(capability_labels: list[str], interaction_hint: str, *, fallback: str) -> str: - """ - 根据能力标签列表构建统一动作提示文本。 - - :param capability_labels: 动作标签列表 - :param interaction_hint: 通用浏览提示 - :param fallback: 缺省提示模板 - :return: 动作提示文本 - """ - if not capability_labels: - return fallback.format(interaction_hint=interaction_hint) - return f'动作键:{" ".join(capability_labels)} | {interaction_hint}' - - @staticmethod - def build_action_result_message_line(label: str, value: str) -> str: - """ - 构建动作结果详情的单行文案。 - - :param label: 字段标题 - :param value: 字段值 - :return: 文本文案 - """ - return f'{label}: {value}' - - @staticmethod - def build_cli_command_hint(*command_args: str) -> str: - """ - 构建 TUI 中展示的 CLI 命令提示文本。 - - :param command_args: CLI 命令参数 - :return: 可直接复制执行的命令文本 - """ - return SHELL_TEXT_FORMATTER.format_shell_command(NESTED_CLI_SUPPORT.build_nested_cli_command(*command_args)) - - @staticmethod - def build_command_hint_lines( - *, - scenario: str, - command: str, - guide: str, - ) -> list[str]: - """ - 构建统一的命令入口提示文本。 - - :param scenario: 适用场景说明 - :param command: 推荐命令 - :param guide: 使用说明 - :return: 文本行列表 - """ - return [ - COMMAND_HINT_TITLES['scenario'], - scenario, - '', - COMMAND_HINT_TITLES['command'], - command, - '', - COMMAND_HINT_TITLES['guide'], - guide, - ] - - @staticmethod - def build_labeled_value_line(label: str, value: str) -> str: - """ - 构建通用的“标签: 值”文案。 - - :param label: 字段标题 - :param value: 字段值 - :return: 文本文案 - """ - return f'{label}: {value}' - - @staticmethod - def build_action_result_toast(label: str, outcome: str, message: str) -> str: - """ - 构建动作完成后的通知文案。 - - :param label: 动作标题 - :param outcome: 执行结果 - :param message: 结果摘要 - :return: 通知文案 - """ - return f'{ACTION_FEEDBACK_COPY["toast_prefix"]} · {label} · {outcome} · {message}' diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/fragments.py b/shierfeng-fastapi-backend/build/lib/cli/tui/copy/fragments.py deleted file mode 100644 index c80c54b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/fragments.py +++ /dev/null @@ -1,339 +0,0 @@ -VIEW_LABELS = { - 'dashboard': '总览', - 'app': '应用', - 'ops': '运维', - 'database': '数据库', - 'cache': '缓存', - 'jobs': '任务', - 'gen': '代码生成', - 'configs': '参数配置', - 'crypto': '加密', -} - -STATUS_LABELS = { - 'ok': '正常', - 'success': '正常', - 'healthy': '正常', - 'fail': '失败', - 'error': '失败', - 'down': '失败', - 'warn': '警告', - 'warning': '警告', - 'degraded': '警告', - 'info': '信息', -} - -GENERAL_COPY = { - 'action_confirm_cancel_label': '取消', - 'action_confirm_hint': '按键提示 · [Enter] 确认执行 [Esc] 取消返回', - 'more_detail_hint': '> 进入对应页面查看更多', - 'tui_command_help': '进入只读巡检工作台', - 'tui_missing_dependency_hint': '请重新执行 `pip install -r requirements.txt`', - 'tui_missing_dependency_message': '当前环境未安装 TUI 可选依赖,无法启动 `shierfeng tui`', -} - -STATE_SUGGESTIONS = { - 'dashboard_empty': '按 [R] 刷新,或进入对应页面查看更多上下文', - 'dashboard_failure': '按 [R] 刷新,或进入对应页面查看完整诊断信息', - 'empty': '可按 [R] 刷新,或切换到其他页面继续查看', - 'failure': '可按 [R] 刷新,或切换页面后重新进入', - 'loading': '可继续切换其他区域,当前内容加载完成后会自动刷新', -} - -PAGE_SUBTITLES = { - 'crypto': '查看运行校验、公钥身份和兼容版本信息', - 'ops': '查看基础连通性、依赖版本与服务器资源', -} - -NAVIGATION_DESCRIPTIONS = { - 'dashboard': '全链路态势与关键健康信号', - 'app': '环境解析、配置快照、补全诊断与路由摘要', - 'ops': '健康检查、依赖版本与服务器资源', - 'database': '连接、迁移版本与存储状态', - 'cache': 'Redis 容量、热点与命中信号', - 'jobs': '定时任务、执行轨迹与失败样本', - 'gen': '业务表、字段与生成配置', - 'configs': '配置巡检、漂移与异常值', - 'crypto': '传输加密校验、公钥身份与兼容版本', -} - -DASHBOARD_HERO_COPY = { - 'subtitle': '先看整体风险和巡检结论,再进入数据库、缓存、任务等分区继续定位', - 'title': '运行驾驶舱', -} - -STATUS_PANEL_COPY = { - 'empty': '暂无数据', -} - -ACTION_LABELS = { - 'app_run': '直接启动应用', - 'app_run_wizard': '打开启动向导', - 'cache_clear_wizard': '打开缓存清理向导', - 'cache_warmup': '执行缓存预热', - 'completion_install': '安装当前 Shell 补全', - 'config_sync': '刷新参数缓存', - 'crypto_keygen': '打开密钥生成入口', - 'crypto_rotate_dry_run': '执行轮换预演', - 'db_init_dry_run': '执行初始化预演', - 'db_upgrade_wizard': '打开升级向导', - 'gen_export_dry_run': '执行导出预演', - 'gen_export_wizard': '打开导出向导', - 'gen_import_wizard': '打开导入向导', - 'gen_sync_db': '同步数据库表结构', - 'job_pause': '暂停任务', - 'job_resume': '恢复任务', - 'job_run_once': '执行一次任务', - 'job_sync': '同步调度配置', - 'ops_ping_db': '执行数据库探活', - 'ops_ping_redis': '执行 Redis 探活', - 'prod_check_wizard': '打开生产巡检向导', -} - -ACTION_SCOPE_LABELS = { - 'cache_warmup': '系统字典与系统参数基础缓存', - 'completion_install': '当前终端活动 Shell 的补全安装流程', - 'config_sync': '当前环境全部系统参数', - 'db_init_dry_run': '当前环境数据库初始化流程', - 'gen_export_dry_run': '当前业务表的代码导出结果预演', - 'gen_sync_db': '当前业务表对应的生成配置和物理表结构', - 'job_sync': '当前环境调度器配置', - 'ops_ping_db': '当前环境数据库连接与基础读写探活', - 'ops_ping_redis': '当前环境 Redis 连接与基础命令探活', -} - -ACTION_PURPOSE_LABELS = { - 'cache_warmup': '重建运行所需的基础缓存内容', - 'completion_install': '自动识别当前 Shell,写入补全脚本并按需更新 rc 文件激活命令', - 'config_sync': '将数据库中的参数值重新写入 Redis 缓存', - 'db_init_dry_run': '仅预演初始化到最新迁移版本,不直接执行真实升级', - 'gen_export_dry_run': '先演练导出流程并确认输出模式、模板数量和结果摘要', - 'gen_sync_db': '将数据库中的最新字段结构同步回当前业务表配置', - 'job_sync': '将最新任务配置同步到调度执行器', - 'ops_ping_db': '快速确认当前环境数据库是否可连接并返回基础探活结果', - 'ops_ping_redis': '快速确认当前环境 Redis 是否可连接并返回基础命令探活结果', -} - -ACTION_PREVIEW_TITLES = { - 'command': '## 执行命令', - 'consequence': '## 确认后果', - 'env': '## 执行环境', - 'summary': '## 动作信息', -} - -ACTION_PREVIEW_FIELD_LABELS = { - 'current_status': '当前状态', - 'job': '任务', - 'job_id': '任务 ID', - 'purpose': '用途', - 'scope': '作用范围', - 'target_action': '目标动作', - 'target_env': '目标环境', -} - -ACTION_RESULT_FIELD_LABELS = { - 'count': '影响数量', - 'exit_code': '退出码', - 'fail': '失败', - 'hint': '建议', - 'job_id': '任务 ID', - 'name': '动作名称', - 'operation': '操作标签', - 'outcome': '结果', - 'service': '服务反馈', - 'success': '成功', - 'summary': '摘要', -} - -ACTION_CONSEQUENCE_TEXTS = { - 'external': '确认后会暂时挂起 TUI,并在当前终端中执行对应命令;命令结束后自动返回工作台。', - 'preview': '确认后会立即执行对应 CLI 低风险命令,并按结果刷新当前页面。', - 'wizard': '确认后会暂时挂起 TUI,并在当前终端中打开对应向导;向导结束后自动返回工作台。', -} - -APP_BINDING_LABELS = { - 'next': '下一页', - 'previous': '上一页', - 'quit': '退出', - 'refresh': '刷新', - 'sidebar': '侧边栏', -} - -INTERNAL_BINDING_LABELS = { - 'action_global': '同步动作', - 'action_primary': '执行动作', - 'action_secondary': '切换状态', - 'action_utility': '工具动作', - 'clear_search': '清空搜索', - 'confirm_cancel': '取消', - 'confirm_submit': '确认', - 'end': '跳到末尾', - 'filter_1': '筛选项 1', - 'filter_2': '筛选项 2', - 'filter_3': '筛选项 3', - 'filter_4': '筛选项 4', - 'focus_left': '向左聚焦', - 'focus_right': '向右聚焦', - 'home': '回到顶部', - 'page_down': '下翻页', - 'page_up': '上翻页', - 'scroll_down': '向下滚动', - 'scroll_up': '向上滚动', - 'search': '页内搜索', -} - -BROWSER_EMPTY_RECORD_COPY = { - 'detail': '当前页面没有可浏览记录', - 'label': '记录列表', - 'summary': '当前页面没有可浏览记录', - 'title': '暂无记录数据', - 'value': '暂无数据', -} - -BROWSER_LOADING_COPY = { - 'detail': '正在后台加载当前记录详情,请稍候', - 'label': '详情状态', - 'title': '详情加载中', - 'value': '加载中', -} - -DETAIL_EMPTY_SECTION_COPY = { - 'detail_page': '当前页面没有可展示的分区详情', - 'detail_record': '当前记录没有可展示的分区详情', - 'label': '分区详情', - 'title': '暂无分区数据', - 'value': '暂无内容', -} - -CAPABILITY_LABELS = { - 'app_run': ACTION_LABELS['app_run'], - 'app_run_wizard': ACTION_LABELS['app_run_wizard'], - 'cache_clear_wizard': ACTION_LABELS['cache_clear_wizard'], - 'cache_warmup': ACTION_LABELS['cache_warmup'], - 'completion_install': ACTION_LABELS['completion_install'], - 'config_sync': ACTION_LABELS['config_sync'], - 'crypto_keygen': ACTION_LABELS['crypto_keygen'], - 'crypto_rotate_dry_run': ACTION_LABELS['crypto_rotate_dry_run'], - 'db_init_dry_run': ACTION_LABELS['db_init_dry_run'], - 'db_upgrade_wizard': ACTION_LABELS['db_upgrade_wizard'], - 'gen_export_dry_run': ACTION_LABELS['gen_export_dry_run'], - 'gen_export_wizard': ACTION_LABELS['gen_export_wizard'], - 'gen_import_wizard': ACTION_LABELS['gen_import_wizard'], - 'gen_sync_db': ACTION_LABELS['gen_sync_db'], - 'job_run_once': ACTION_LABELS['job_run_once'], - 'job_toggle': '暂停/恢复任务', - 'job_sync': ACTION_LABELS['job_sync'], - 'ops_ping_db': ACTION_LABELS['ops_ping_db'], - 'ops_ping_redis': ACTION_LABELS['ops_ping_redis'], - 'prod_check_wizard': ACTION_LABELS['prod_check_wizard'], -} - -CAPABILITY_HINT_LABELS = { - 'app_run': '直接启动', - 'app_run_wizard': '打开启动向导', - 'cache_clear_wizard': '清理向导', - 'cache_warmup': '执行缓存预热', - 'completion_install': '安装补全', - 'config_sync': '刷新参数缓存', - 'crypto_keygen': '密钥生成', - 'crypto_rotate_dry_run': '执行轮换预演', - 'db_init_dry_run': '初始化预演', - 'db_upgrade_wizard': '打开升级向导', - 'gen_export_dry_run': '导出预演', - 'gen_export_wizard': '导出向导', - 'gen_import_wizard': '导入向导', - 'gen_sync_db': '同步表结构', - 'job_run_once': '执行一次', - 'job_toggle': '暂停/恢复', - 'job_sync': '同步调度', - 'ops_ping_db': '数据库探活', - 'ops_ping_redis': 'Redis 探活', - 'prod_check_wizard': '打开生产巡检向导', -} - -WORKSPACE_LABELS = { - 'current_section': '当前分区', - 'menu': '菜单', - 'overview': '概览', - 'record': '记录', - 'section': '分区', - 'shortcut': '快捷键', - 'status': '状态', -} - -WORKSPACE_TITLES = { - 'action_feedback': '【执行反馈】', - 'detail_content': '详情内容', - 'key_fields': '【关键字段】', - 'key_info': '【关键信息】', -} - -WORKSPACE_EMPTY_TEXTS = { - 'actions': '暂无可执行动作', - 'detail': '暂无详情', - 'key_info': '暂无关键信息', -} - -STATE_SECTION_TITLES = { - 'detail': '## 补充说明', - 'diagnostic': '## 诊断线索', - 'error': '## 错误摘要', - 'status': '## 当前状态', - 'suggestion': '## 下一步', -} - -ACTION_NOTIFICATION_COPY = { - 'empty_line': '当前记录没有可执行的低风险动作', - 'title': 'TUI 动作', - 'unavailable_message': '当前页面没有可执行的对应快捷动作', -} - -BROWSER_ACTION_PANEL_COPY = { - 'action_section_title': '## 可执行动作', - 'operation_section_title': '## 浏览操作', - 'recent_action_section_title': '## 最近动作反馈', -} - -COMMAND_HINT_TITLES = { - 'command': '## 推荐命令', - 'guide': '## 使用说明', - 'scenario': '## 适用场景', -} - -SIGNAL_RAIL_COPY = { - 'empty': '暂无实时信号', - 'title': '系统信号带', -} - -METRIC_COPY = { - 'hint_label': '说明', - 'label': '指标', - 'value_label': '数值', -} - -ACTION_HINT_COPY = { - 'browser_default': '当前页面以浏览为主。{interaction_hint}', - 'browser_templates': { - 'cache': '建议先核对 Redis 概览与键值样本,再决定预热或进入清理向导。{interaction_hint}', - 'configs': '建议先看高风险配置,再决定是否刷新参数缓存。{interaction_hint}', - 'gen': '建议先看生成前校验、同步预检查与代码预览,再决定是否进入导入向导、同步表结构或进入导出向导。{interaction_hint}', - 'jobs': '建议先看失败聚合,再决定执行一次、暂停恢复或同步调度。{interaction_hint}', - }, - 'detail_default': '当前页面以查看分区为主。{interaction_hint}', - 'detail_templates': { - 'app': '建议先确认环境映射、应用配置、启动前检查、补全诊断和路由状态,再决定是安装补全、直接启动应用还是进入启动向导。{interaction_hint}', - 'crypto': '建议先确认运行校验、公钥身份和兼容版本,再决定是否生成新密钥或执行轮换预演。{interaction_hint}', - 'database': '建议先确认 revision、Heads 和历史链路,再决定是否执行初始化预演或进入升级向导。{interaction_hint}', - 'ops': '建议先确认数据库探活、Redis 探活、依赖版本和服务器资源,再决定是否进入生产巡检。{interaction_hint}', - }, - 'operation_hint_lines': [ - '> [←/→] 切换焦点', - '> [J/K] 向上/向下滚动', - '> [PgUp/PgDn] 整页滚动', - '> [Home/End] 跳到首尾', - ], -} - -ACTION_FEEDBACK_COPY = { - 'toast_prefix': '动作结果', -} diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/navigation.py b/shierfeng-fastapi-backend/build/lib/cli/tui/copy/navigation.py deleted file mode 100644 index f335066..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/navigation.py +++ /dev/null @@ -1,133 +0,0 @@ -from cli.tui.copy.fragments import ( - APP_BINDING_LABELS, - GENERAL_COPY, - INTERNAL_BINDING_LABELS, - NAVIGATION_DESCRIPTIONS, - STATUS_LABELS, - VIEW_LABELS, -) - - -class TuiNavigationCopyMixin: - """ - TUI 导航与基础标签文案混入。 - """ - - @staticmethod - def render_view_label(view_key: str) -> str: - """ - 将内部视图标识转换为中文显示名称。 - - :param view_key: 视图标识 - :return: 中文视图名称 - """ - normalized_view = str(view_key).strip().lower() - return VIEW_LABELS.get(normalized_view, view_key) - - @staticmethod - def render_status_label(status: str, fallback: str = '信息') -> str: - """ - 将状态值转换为中文显示文本。 - - :param status: 原始状态值 - :param fallback: 缺失时兜底文本 - :return: 中文状态文本 - """ - normalized_status = str(status).strip().lower() - if not normalized_status: - return fallback - return STATUS_LABELS.get(normalized_status, str(status).strip().upper()) - - @staticmethod - def render_status_code(status: str) -> str: - """ - 将状态值转换为控制台风格状态码。 - - :param status: 原始状态值 - :return: 状态码文本 - """ - normalized_status = str(status).strip().lower() - status_code_mapping = { - 'ok': 'SYS.OK', - 'success': 'SYS.OK', - 'healthy': 'SYS.OK', - 'fail': 'SYS.FAIL', - 'error': 'SYS.FAIL', - 'down': 'SYS.FAIL', - 'warn': 'SYS.WARN', - 'warning': 'SYS.WARN', - 'degraded': 'SYS.WARN', - 'info': 'SYS.INFO', - } - return status_code_mapping.get(normalized_status, f'SYS.{normalized_status.upper() or "INFO"}') - - @staticmethod - def render_navigation_description(view_key: str) -> str: - """ - 获取指定视图的导航说明。 - - :param view_key: 视图标识 - :return: 导航说明 - """ - normalized_view = str(view_key).strip().lower() - return NAVIGATION_DESCRIPTIONS.get(normalized_view, '') - - @staticmethod - def build_tui_command_help() -> str: - """ - 构建 `shierfeng tui` 命令帮助文案。 - - :return: 命令帮助文案 - """ - return GENERAL_COPY['tui_command_help'] - - @staticmethod - def build_missing_dependency_message() -> str: - """ - 构建 TUI 可选依赖缺失时的提示消息。 - - :return: 缺依赖提示消息 - """ - return GENERAL_COPY['tui_missing_dependency_message'] - - @staticmethod - def build_missing_dependency_hint() -> str: - """ - 构建 TUI 可选依赖缺失时的恢复建议。 - - :return: 恢复建议文案 - """ - return GENERAL_COPY['tui_missing_dependency_hint'] - - @staticmethod - def build_app_binding_label(binding_key: str) -> str: - """ - 根据绑定标识构建应用级快捷键标签文案。 - - :param binding_key: 绑定标识 - :return: 快捷键标签文案 - """ - normalized_key = str(binding_key).strip().lower() - return APP_BINDING_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_confirm_binding_label(binding_key: str) -> str: - """ - 根据确认弹窗绑定标识构建快捷键标签文案。 - - :param binding_key: 绑定标识 - :return: 快捷键标签文案 - """ - normalized_key = str(binding_key).strip().lower() - return INTERNAL_BINDING_LABELS.get(f'confirm_{normalized_key}', normalized_key) - - @staticmethod - def build_internal_binding_label(binding_key: str) -> str: - """ - 根据内部交互绑定标识构建快捷键标签文案。 - - :param binding_key: 绑定标识 - :return: 快捷键标签文案 - """ - normalized_key = str(binding_key).strip().lower() - return INTERNAL_BINDING_LABELS.get(normalized_key, normalized_key) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/workspace.py b/shierfeng-fastapi-backend/build/lib/cli/tui/copy/workspace.py deleted file mode 100644 index e4f0ff1..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/copy/workspace.py +++ /dev/null @@ -1,593 +0,0 @@ -from cli.tui.copy.fragments import ( - ACTION_HINT_COPY, - BROWSER_ACTION_PANEL_COPY, - BROWSER_EMPTY_RECORD_COPY, - BROWSER_LOADING_COPY, - DASHBOARD_HERO_COPY, - DETAIL_EMPTY_SECTION_COPY, - GENERAL_COPY, - METRIC_COPY, - SIGNAL_RAIL_COPY, - STATE_SECTION_TITLES, - STATE_SUGGESTIONS, - STATUS_PANEL_COPY, - WORKSPACE_EMPTY_TEXTS, - WORKSPACE_LABELS, - WORKSPACE_TITLES, -) - - -class TuiWorkspaceCopyMixin: - """ - TUI 工作区与状态文案混入。 - """ - - @staticmethod - def build_workspace_label(label_key: str) -> str: - """ - 根据工作台标签标识构建文案。 - - :param label_key: 标签标识 - :return: 标签文案 - """ - normalized_key = str(label_key).strip().lower() - return WORKSPACE_LABELS.get(normalized_key, normalized_key) - - @staticmethod - def build_workspace_title(title_key: str) -> str: - """ - 根据工作台标题标识构建文案。 - - :param title_key: 标题标识 - :return: 标题文案 - """ - normalized_key = str(title_key).strip().lower() - return WORKSPACE_TITLES.get(normalized_key, normalized_key) - - @staticmethod - def build_workspace_empty_text(text_key: str) -> str: - """ - 根据工作台空态标识构建文案。 - - :param text_key: 空态标识 - :return: 空态文案 - """ - normalized_key = str(text_key).strip().lower() - return WORKSPACE_EMPTY_TEXTS.get(normalized_key, normalized_key) - - @staticmethod - def build_status_panel_empty_text() -> str: - """ - 构建状态面板空态提示文案。 - - :return: 空态提示文案 - """ - return STATUS_PANEL_COPY['empty'] - - @staticmethod - def build_signal_rail_empty_text() -> str: - """ - 构建信号带空态提示文案。 - - :return: 空态提示文案 - """ - return SIGNAL_RAIL_COPY['empty'] - - @staticmethod - def build_more_detail_hint() -> str: - """ - 构建查看更多提示文案。 - - :return: 提示文案 - """ - return GENERAL_COPY['more_detail_hint'] - - @staticmethod - def build_empty_state_suggestion() -> str: - """ - 构建通用空态建议文案。 - - :return: 建议文案 - """ - return STATE_SUGGESTIONS['empty'] - - @staticmethod - def build_loading_state_suggestion() -> str: - """ - 构建通用加载态建议文案。 - - :return: 建议文案 - """ - return STATE_SUGGESTIONS['loading'] - - @staticmethod - def build_failure_state_suggestion() -> str: - """ - 构建通用失败态建议文案。 - - :return: 建议文案 - """ - return STATE_SUGGESTIONS['failure'] - - @staticmethod - def build_dashboard_failure_suggestion() -> str: - """ - 构建首页失败态建议文案。 - - :return: 建议文案 - """ - return STATE_SUGGESTIONS['dashboard_failure'] - - @staticmethod - def build_dashboard_empty_suggestion() -> str: - """ - 构建首页空态建议文案。 - - :return: 建议文案 - """ - return STATE_SUGGESTIONS['dashboard_empty'] - - @staticmethod - def build_state_section_title(title_key: str) -> str: - """ - 根据状态区块标识构建标题文案。 - - :param title_key: 区块标识 - :return: 标题文案 - """ - normalized_key = str(title_key).strip().lower() - return STATE_SECTION_TITLES.get(normalized_key, normalized_key) - - @staticmethod - def build_dashboard_hero_title() -> str: - """ - 构建首页 Hero 标题文案。 - - :return: 标题文案 - """ - return DASHBOARD_HERO_COPY['title'] - - @staticmethod - def build_dashboard_hero_subtitle() -> str: - """ - 构建首页 Hero 副标题文案。 - - :return: 副标题文案 - """ - return DASHBOARD_HERO_COPY['subtitle'] - - @staticmethod - def build_browser_empty_record_copy(field_key: str) -> str: - """ - 根据字段标识构建浏览页空记录文案。 - - :param field_key: 字段标识 - :return: 文案 - """ - normalized_key = str(field_key).strip().lower() - return BROWSER_EMPTY_RECORD_COPY.get(normalized_key, normalized_key) - - @staticmethod - def build_browser_loading_copy(field_key: str) -> str: - """ - 根据字段标识构建浏览页加载占位文案。 - - :param field_key: 字段标识 - :return: 文案 - """ - normalized_key = str(field_key).strip().lower() - return BROWSER_LOADING_COPY.get(normalized_key, normalized_key) - - @staticmethod - def build_detail_empty_section_copy(field_key: str) -> str: - """ - 根据字段标识构建详情页空态文案。 - - :param field_key: 字段标识 - :return: 文案 - """ - normalized_key = str(field_key).strip().lower() - return DETAIL_EMPTY_SECTION_COPY.get(normalized_key, normalized_key) - - @staticmethod - def build_refresh_page_suggestion(page_name: str, detail: str) -> str: - """ - 构建详情页场景下的统一建议操作文案。 - - :param page_name: 页面名称 - :param detail: 后续动作描述 - :return: 建议文案 - """ - return f'可按 [R] 刷新,或进入{page_name}页面{detail}' - - @staticmethod - def build_dashboard_page_suggestion(page_name: str, detail: str) -> str: - """ - 构建首页总览面板场景下的统一建议操作文案。 - - :param page_name: 页面名称 - :param detail: 后续动作描述 - :return: 建议文案 - """ - return f'按 [R] 刷新,或进入{page_name}页面{detail}' - - @staticmethod - def build_unavailable_record_title(resource_name: str) -> str: - """ - 构建浏览页失败兜底记录标题。 - - :param resource_name: 资源名称 - :return: 标题文本 - """ - return f'{resource_name}数据暂不可用' - - @staticmethod - def build_load_failure_section_title(resource_name: str) -> str: - """ - 构建浏览页失败兜底分区标题。 - - :param resource_name: 资源名称 - :return: 分区标题 - """ - return f'{resource_name}加载失败' - - @staticmethod - def build_empty_record_title(resource_name: str) -> str: - """ - 构建浏览页空记录标题。 - - :param resource_name: 资源名称 - :return: 标题文本 - """ - return f'暂无{resource_name}' - - @staticmethod - def build_unavailable_subtitle(resource_name: str, message: str) -> str: - """ - 构建浏览页数据不可用副标题。 - - :param resource_name: 资源名称 - :param message: 错误摘要 - :return: 副标题文本 - """ - return f'{resource_name}数据不可用:{message}' - - @staticmethod - def build_empty_record_summary(detail: str) -> str: - """ - 构建浏览页空记录摘要。 - - :param detail: 摘要说明 - :return: 摘要文本 - """ - return detail - - @staticmethod - def build_loaded_collection_subtitle(count: int, unit: str, detail: str) -> str: - """ - 构建浏览页“已加载”副标题。 - - :param count: 数量 - :param unit: 单位 - :param detail: 补充说明 - :return: 副标题文本 - """ - return f'已加载 {count} {unit},{detail}' - - @staticmethod - def build_summary_with_message(summary: str, message: str) -> str: - """ - 构建带结果摘要补充的副标题。 - - :param summary: 主摘要 - :param message: 结果消息 - :return: 副标题文本 - """ - return f'{summary} | {message}' - - @staticmethod - def build_count_detail_subtitle(prefix: str, count: int, unit: str, suffix: str) -> str: - """ - 构建带数量的详情页副标题。 - - :param prefix: 前缀文本 - :param count: 数量 - :param unit: 数量单位 - :param suffix: 后缀文本 - :return: 副标题文本 - """ - return f'{prefix} {count} {unit}{suffix}' - - @staticmethod - def build_value_detail_subtitle(prefix: str, value: str, suffix: str) -> str: - """ - 构建带单值信息的详情页副标题。 - - :param prefix: 前缀文本 - :param value: 值文本 - :param suffix: 后缀文本 - :return: 副标题文本 - """ - return f'{prefix} {value}{suffix}' - - @staticmethod - def build_workspace_hero_lines( - *, - view_label: str, - title: str, - subtitle: str, - env: str, - summary: str, - refreshed_at: str, - shortcut_hint: str, - ) -> list[str]: - """ - 构建工作区顶部摘要文本行。 - - :param view_label: 当前页面显示名称 - :param title: 页面标题 - :param subtitle: 页面副标题 - :param env: 当前环境 - :param summary: 页面摘要 - :param refreshed_at: 刷新时间 - :param shortcut_hint: 快捷键提示 - :return: 文本行列表 - """ - return [ - f'[{view_label} 控制台]', - title, - subtitle, - '', - f'运行环境 · {env}', - f'当前页面 · {view_label}', - f'运行摘要 · {summary}', - f'操作提示 · {shortcut_hint}', - f'最近刷新 · {refreshed_at}', - ] - - @staticmethod - def build_navigation_item_lines(index: int, label: str, shortcut: str, description: str) -> list[str]: - """ - 构建左侧导航项文案。 - - :param index: 导航索引 - :param label: 导航标题 - :param shortcut: 快捷键 - :param description: 导航说明 - :return: 文本行列表 - """ - return [ - f'▌ {WORKSPACE_LABELS["menu"]} {index + 1:02d} · {label}', - f'│ {WORKSPACE_LABELS["shortcut"]} [{shortcut.upper()}] · {description}', - ] - - @staticmethod - def build_section_item_lines(index: int, title: str, status_badge: str, preview: str) -> list[str]: - """ - 构建详情页分区导航项文案。 - - :param index: 分区索引 - :param title: 分区标题 - :param status_badge: 状态徽标 - :param preview: 预览文本 - :return: 文本行列表 - """ - return [ - f'▌ {WORKSPACE_LABELS["section"]} {index + 1:02d} · {title}', - f'│ {status_badge} {preview}', - ] - - @staticmethod - def build_record_item_lines(index: int, title: str, status_badge: str, summary: str) -> list[str]: - """ - 构建浏览页记录导航项文案。 - - :param index: 记录索引 - :param title: 记录标题 - :param status_badge: 状态徽标 - :param summary: 记录摘要 - :return: 文本行列表 - """ - return [ - f'▌ {WORKSPACE_LABELS["record"]} {index + 1:02d} · {title}', - f'│ {status_badge} {summary}', - ] - - @staticmethod - def build_section_detail_lines(title: str, status_label: str, status_badge: str, body: str) -> list[str]: - """ - 构建详情页右侧分区内容文案。 - - :param title: 分区标题 - :param status_label: 分区状态 - :param status_badge: 状态徽标 - :param body: 主体文本 - :return: 文本行列表 - """ - return [ - f'{status_badge} {title}', - f'{WORKSPACE_LABELS["status"]} · {status_label}', - '', - body, - ] - - @staticmethod - def build_browser_action_panel_lines(action_lines: list[str], feedback_lines: list[str]) -> list[str]: - """ - 构建浏览页动作面板文案。 - - :param action_lines: 可执行动作文本 - :param feedback_lines: 最近动作反馈文本 - :return: 文本行列表 - """ - lines = [ - BROWSER_ACTION_PANEL_COPY['action_section_title'], - *[f'> {line}' for line in action_lines], - '', - BROWSER_ACTION_PANEL_COPY['operation_section_title'], - ] - lines.extend(ACTION_HINT_COPY['operation_hint_lines']) - if feedback_lines: - lines.extend( - [ - '', - BROWSER_ACTION_PANEL_COPY['recent_action_section_title'], - *[f'> {line}' for line in feedback_lines], - ] - ) - return lines - - @staticmethod - def build_status_panel_text(title: str, status_code: str, status_label: str, body: str) -> str: - """ - 构建状态面板渲染文本。 - - :param title: 面板标题 - :param status_code: 状态码 - :param status_label: 状态标签 - :param body: 面板正文 - :return: 渲染文本 - """ - return f'{status_code} {title}\n{WORKSPACE_LABELS["status"]} · {status_label}\n\n{body}' - - @staticmethod - def build_metric_panel_text(title: str, value: str, status_code: str, status_label: str, hint: str) -> str: - """ - 构建指标卡渲染文本。 - - :param title: 指标标题 - :param value: 指标值 - :param status_code: 状态码 - :param status_label: 状态标签 - :param hint: 提示文本 - :return: 渲染文本 - """ - return ( - f'{METRIC_COPY["label"]} · {title}\n' - f'────────────────────────\n' - f'{METRIC_COPY["value_label"]} · {value}\n' - f'{WORKSPACE_LABELS["status"]} · {status_code} / {status_label}\n' - f'{METRIC_COPY["hint_label"]} · {hint}' - ) - - @staticmethod - def build_signal_rail_text(pulse: str, body: str) -> str: - """ - 构建首页信号带渲染文本。 - - :param pulse: 当前脉冲字符 - :param body: 主体文本 - :return: 渲染文本 - """ - return '\n'.join( - [ - f'{SIGNAL_RAIL_COPY["title"]} {pulse}', - '────────────────────────', - body or SIGNAL_RAIL_COPY['empty'], - ] - ) - - @staticmethod - def build_workspace_header_lines( - *, - env: str, - view_label: str, - timestamp: str, - shortcut_hint: str, - ) -> list[str]: - """ - 构建工作区顶部状态栏文本行。 - - :param env: 当前环境 - :param view_label: 当前页面显示名称 - :param timestamp: 当前时间 - :param shortcut_hint: 快捷键提示 - :return: 文本行列表 - """ - return [ - f'ShiErFeng 控制台 · 环境 {env.upper()} · 页面 {view_label}', - f'快捷键 · {shortcut_hint} 时间 {timestamp}', - ] - - @staticmethod - def build_dashboard_signal_lines( - *, - status_track: str, - fail_count: int, - warn_count: int, - ok_count: int, - total_count: int, - navigation_shortcut_hint: str, - ) -> list[str]: - """ - 构建首页信号带文案。 - - :param status_track: 面板状态轨迹 - :param fail_count: 失败数量 - :param warn_count: 警告数量 - :param ok_count: 正常数量 - :param total_count: 总数量 - :param navigation_shortcut_hint: 页面快捷键提示 - :return: 文本行列表 - """ - return [ - f'面板轨迹 · {status_track or "-"}', - f'风险分布 · 失败 {fail_count:02d} 警告 {warn_count:02d} 正常 {ok_count:02d} 合计 {total_count:02d}', - f'快捷入口 · 页面 {navigation_shortcut_hint}', - ] - - @staticmethod - def build_dashboard_summary_text(total_count: int, ok_count: int, warn_count: int, fail_count: int) -> str: - """ - 构建首页摘要文本。 - - :param total_count: 面板总数 - :param ok_count: 正常数量 - :param warn_count: 警告数量 - :param fail_count: 失败数量 - :return: 摘要文本 - """ - posture = '存在风险' if fail_count else '需要关注' if warn_count else '运行稳定' - return ( - f'当前态势:{posture}。共 {total_count} 个面板,正常 {ok_count} 个,' - f'警告 {warn_count} 个,失败 {fail_count} 个。' - ) - - @staticmethod - def build_detail_summary_text(total_count: int, ok_count: int, warn_count: int, fail_count: int) -> str: - """ - 构建详情页摘要文本。 - - :param total_count: 分区总数 - :param ok_count: 正常数量 - :param warn_count: 警告数量 - :param fail_count: 失败数量 - :return: 摘要文本 - """ - return ( - f'当前页共 {total_count} 个分区,正常 {ok_count} 个,警告 {warn_count} 个,失败 {fail_count} 个。' - '左侧上下切页,左右切换当前焦点区域。' - ) - - @staticmethod - def build_browser_summary_text( - total_count: int, - ok_count: int, - warn_count: int, - fail_count: int, - action_hint: str, - ) -> str: - """ - 构建浏览页摘要文本。 - - :param total_count: 记录总数 - :param ok_count: 正常数量 - :param warn_count: 警告数量 - :param fail_count: 失败数量 - :param action_hint: 动作提示文案 - :return: 摘要文本 - """ - return ( - f'当前页共 {total_count} 条记录,正常 {ok_count} 条,警告 {warn_count} 条,失败 {fail_count} 条。' - f'切换记录后,右侧会联动展示概览与分区详情。{action_hint}' - ) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/diagnostics.py b/shierfeng-fastapi-backend/build/lib/cli/tui/diagnostics.py deleted file mode 100644 index 05f8b3d..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/diagnostics.py +++ /dev/null @@ -1,581 +0,0 @@ -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) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/keymaps.py b/shierfeng-fastapi-backend/build/lib/cli/tui/keymaps.py deleted file mode 100644 index e8392c1..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/keymaps.py +++ /dev/null @@ -1,177 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass(frozen=True) -class TuiKeymapRegistry: - """ - TUI 键位注册表。 - - 该对象集中维护页面切换快捷键和派生提示文案,避免模块级 - 常量散落并在后续扩展时继续增长。 - - :param navigation_shortcuts: 页面导航快捷键映射 - """ - - navigation_shortcuts: dict[str, str] = field(default_factory=dict) - - @staticmethod - def normalize_view_key(view_key: str) -> str: - """ - 规范化页面标识。 - - :param view_key: 原始页面标识 - :return: 规范化后的页面标识 - """ - return str(view_key).strip().lower() - - def get_navigation_shortcut(self, view_key: str) -> str: - """ - 获取指定页面的导航快捷键。 - - :param view_key: 页面标识 - :return: 快捷键文本 - """ - normalized_view_key = self.normalize_view_key(view_key) - return self.navigation_shortcuts.get(normalized_view_key, '') - - @property - def navigation_shortcut_hint(self) -> str: - """ - 构建页面导航快捷键总提示。 - - :return: 快捷键提示 - """ - ordered_keys = ['dashboard', 'app', 'ops', 'database', 'cache', 'jobs', 'gen', 'configs', 'crypto'] - shortcuts = [ - self.get_navigation_shortcut(view_key).upper() - for view_key in ordered_keys - if self.get_navigation_shortcut(view_key) - ] - return f'[{" / ".join(shortcuts).replace(" / ", "/")}]' - - @property - def sidebar_shortcut_hint(self) -> str: - """ - 获取侧栏快捷键提示。 - - :return: 提示文本 - """ - return '[S] 侧栏' - - @property - def refresh_shortcut_hint(self) -> str: - """ - 获取刷新快捷键提示。 - - :return: 提示文本 - """ - return '[R] 刷新' - - @property - def quit_shortcut_hint(self) -> str: - """ - 获取退出快捷键提示。 - - :return: 提示文本 - """ - return '[Q] 退出' - - @property - def focus_shortcut_hint(self) -> str: - """ - 获取焦点切换快捷键提示。 - - :return: 提示文本 - """ - return '[←/→] 切换焦点' - - @property - def region_switch_shortcut_hint(self) -> str: - """ - 获取区域切换快捷键提示。 - - :return: 提示文本 - """ - return '[←/→] 切换区域' - - @property - def scroll_shortcut_hint(self) -> str: - """ - 获取滚动快捷键提示。 - - :return: 提示文本 - """ - return '[J/K] 滚动' - - @property - def page_scroll_shortcut_hint(self) -> str: - """ - 获取翻页快捷键提示。 - - :return: 提示文本 - """ - return '[PgUp/PgDn] 翻页' - - @property - def home_end_shortcut_hint(self) -> str: - """ - 获取首尾跳转快捷键提示。 - - :return: 提示文本 - """ - return '[Home/End] 首尾' - - @property - def browser_interaction_hint(self) -> str: - """ - 构建浏览页交互提示。 - - :return: 提示文本 - """ - return ( - f'浏览键:{self.focus_shortcut_hint} {self.scroll_shortcut_hint} ' - f'{self.page_scroll_shortcut_hint} {self.home_end_shortcut_hint}' - ) - - @property - def global_shortcut_hint(self) -> str: - """ - 构建全局快捷键提示。 - - :return: 提示文本 - """ - return ( - f'页面 {self.navigation_shortcut_hint} {self.sidebar_shortcut_hint} ' - f'{self.focus_shortcut_hint} {self.scroll_shortcut_hint} ' - f'{self.page_scroll_shortcut_hint} {self.home_end_shortcut_hint} ' - f'{self.refresh_shortcut_hint} {self.quit_shortcut_hint}' - ) - - @property - def hero_shortcut_hint(self) -> str: - """ - 构建首页英雄区快捷键提示。 - - :return: 提示文本 - """ - return ( - f'页面 {self.navigation_shortcut_hint} {self.sidebar_shortcut_hint} ' - f'{self.region_switch_shortcut_hint} {self.scroll_shortcut_hint} ' - f'{self.page_scroll_shortcut_hint} {self.home_end_shortcut_hint} ' - f'{self.refresh_shortcut_hint} {self.quit_shortcut_hint}' - ) - - -TUI_KEYMAP_REGISTRY = TuiKeymapRegistry( - navigation_shortcuts={ - 'dashboard': 'd', - 'app': 'a', - 'ops': 'o', - 'database': 'b', - 'cache': 'c', - 'jobs': 't', - 'gen': 'g', - 'configs': 'p', - 'crypto': 'e', - } -) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/__init__.py deleted file mode 100644 index e26b185..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from cli.tui.screens.browser import BrowserScreen -from cli.tui.screens.dashboard import DashboardScreen -from cli.tui.screens.detail import DetailScreen - -__all__ = ['BrowserScreen', 'DashboardScreen', 'DetailScreen'] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/browser.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/browser.py deleted file mode 100644 index a6f1443..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/browser.py +++ /dev/null @@ -1,840 +0,0 @@ -import asyncio - -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, ScrollableContainer, Vertical -from textual.screen import Screen -from textual.widgets import Footer, Static - -from cli.tui.actions import ( - TUI_ACTION_PRESENTATION_SERVICE, - TUI_ACTION_REGISTRY, - TuiActionResult, - TuiActionSpec, -) -from cli.tui.adapters import BrowserPageSnapshot, BrowserRecordSnapshot, DetailSectionSnapshot -from cli.tui.adapters.models import TUI_ADAPTER_MODEL_RENDERER -from cli.tui.copy import TUI_COPY -from cli.tui.screens.focus import BaseScreenFocusService, ScreenFocusActionsMixin -from cli.tui.screens.interactions import TUI_SCREEN_INTERACTION_SERVICE, ScreenInteractionActionsMixin -from cli.tui.screens.summary import STATUS_SUMMARY_BUILDER -from cli.tui.search import TUI_SEARCH_SERVICE -from cli.tui.widgets import ( - NavigationItem, - RecordNavigator, - RecordSummaryView, - SectionDetailView, - SectionNavigator, - WorkspaceHeader, - WorkspaceHero, - WorkspaceSidebar, -) - - -class BrowserScreenSupport: - """ - 浏览页屏幕支持对象。 - - 该对象负责浏览页摘要、搜索词、动作解析、空态/加载态分区构建和 - 焦点顺序编排,使 `BrowserScreen` 本体主要保留 Textual 事件桥接。 - """ - - @staticmethod - def build_summary_text(snapshot: BrowserPageSnapshot, active_view: str) -> str: - """ - 构建浏览页摘要文本。 - - :param snapshot: 浏览页快照 - :param active_view: 当前激活视图 - :return: 摘要文本 - """ - summary = STATUS_SUMMARY_BUILDER.build(snapshot.records) - return TUI_COPY.build_browser_summary_text( - total_count=summary.total_count, - ok_count=summary.ok_count, - warn_count=summary.warn_count, - fail_count=summary.fail_count, - action_hint=TUI_ACTION_PRESENTATION_SERVICE.build_browser_action_hint(active_view), - ) - - @staticmethod - def build_filter_bar_text(snapshot: BrowserPageSnapshot) -> str: - """ - 构建当前浏览页筛选条文本。 - - :param snapshot: 浏览页快照 - :return: 筛选条文本 - """ - search_context = snapshot.search - return TUI_SEARCH_SERVICE.build_filter_bar_text( - snapshot.filters, - snapshot.active_filter_key, - search_query=search_context.query if search_context is not None else '', - search_placeholder=search_context.placeholder if search_context is not None else '', - search_suggestions=search_context.suggestions if search_context is not None else [], - ) - - @staticmethod - def current_search_query(snapshot: BrowserPageSnapshot) -> str: - """ - 读取当前浏览页搜索词。 - - :param snapshot: 浏览页快照 - :return: 搜索词 - """ - search_context = snapshot.search - return search_context.query if search_context is not None else '' - - @staticmethod - def resolve_action( - *, - active_view: str, - slot: str, - record: BrowserRecordSnapshot, - env: str, - ) -> TuiActionSpec | None: - """ - 解析当前页面指定槽位对应的动作。 - - :param active_view: 当前激活视图 - :param slot: 动作槽位 - :param record: 当前选中记录 - :param env: 当前运行环境 - :return: 动作定义 - """ - return TUI_ACTION_REGISTRY.resolve_browser_action( - view_key=active_view, - slot=slot, # type: ignore[arg-type] - record=record, - env=env, - ) - - @staticmethod - def build_record_action_lines( - *, - active_view: str, - record: BrowserRecordSnapshot, - env: str, - feedback_lines: list[str], - ) -> list[str]: - """ - 构建当前记录动作面板文本。 - - :param active_view: 当前激活视图 - :param record: 当前记录 - :param env: 当前运行环境 - :param feedback_lines: 最近动作反馈文本 - :return: 动作面板文本 - """ - action_lines = TUI_ACTION_PRESENTATION_SERVICE.build_browser_action_lines( - view_key=active_view, - record=record, - env=env, - ) - return TUI_COPY.build_browser_action_panel_lines(action_lines, feedback_lines) - - @staticmethod - def build_empty_record() -> BrowserRecordSnapshot: - """ - 构建浏览页空态兜底记录。 - - :return: 记录快照 - """ - return BrowserRecordSnapshot( - key='empty', - title=TUI_COPY.build_browser_empty_record_copy('title'), - status='info', - summary=TUI_COPY.build_browser_empty_record_copy('summary'), - metadata_lines=[], - detail_sections=[ - DetailSectionSnapshot( - title=TUI_COPY.build_browser_empty_record_copy('title'), - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label=TUI_COPY.build_browser_empty_record_copy('label'), - empty_value=TUI_COPY.build_browser_empty_record_copy('value'), - detail=TUI_COPY.build_browser_empty_record_copy('detail'), - ), - ) - ], - ) - - @staticmethod - def build_loading_section() -> DetailSectionSnapshot: - """ - 构建详情加载中的占位分区。 - - :return: 占位分区 - """ - return DetailSectionSnapshot( - title=TUI_COPY.build_browser_loading_copy('title'), - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_loading_lines( - loading_label=TUI_COPY.build_browser_loading_copy('label'), - loading_value=TUI_COPY.build_browser_loading_copy('value'), - detail=TUI_COPY.build_browser_loading_copy('detail'), - ), - ) - - @staticmethod - def build_detail_load_failure_sections(error: Exception, resource_name: str) -> list[DetailSectionSnapshot]: - """ - 构建详情加载失败时的兜底分区。 - - :param error: 后台加载异常 - :param resource_name: 资源名称 - :return: 失败分区列表 - """ - return [ - DetailSectionSnapshot( - title=TUI_COPY.build_load_failure_section_title(resource_name), - status='fail', - lines=TUI_ADAPTER_MODEL_RENDERER.build_failure_lines( - { - 'message': '后台详情加载失败', - 'error': str(error) or error.__class__.__name__, - 'hint': '可按 [R] 刷新当前页面,或切换记录后重试', - }, - empty_label='详情状态', - empty_value='不可用', - ), - ) - ] - - @staticmethod - def build_empty_section() -> DetailSectionSnapshot: - """ - 构建浏览页详情空态兜底分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title=TUI_COPY.build_detail_empty_section_copy('title'), - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label=TUI_COPY.build_detail_empty_section_copy('label'), - empty_value=TUI_COPY.build_detail_empty_section_copy('value'), - detail=TUI_COPY.build_detail_empty_section_copy('detail_record'), - ), - ) - - -class BrowserScreenFocusService(BaseScreenFocusService): - """ - 浏览页焦点服务。 - - 该对象负责定义浏览页可聚焦组件顺序,以及按左右键和滚动键解析 - 当前目标区域。 - """ - - def get_focus_order( - self, - screen: 'BrowserScreen', - ) -> list[WorkspaceSidebar | RecordNavigator | SectionNavigator | RecordSummaryView | SectionDetailView]: - """ - 定义浏览页可通过左右键切换的焦点顺序。 - - :param screen: 当前浏览页屏幕 - :return: 焦点顺序列表 - """ - return [ - screen.query_one(WorkspaceSidebar), - screen.query_one(RecordNavigator), - screen.query_one(SectionNavigator), - screen.query_one(RecordSummaryView), - screen.query_one(SectionDetailView), - ] - - def get_default_scroll_target( - self, - screen: 'BrowserScreen', - ) -> ScrollableContainer: - """ - 返回浏览页默认滚动容器。 - - :param screen: 当前浏览页屏幕 - :return: 主工作区滚动容器 - """ - return screen.query_one('#workspace-main', ScrollableContainer) - - -class BrowserScreen(ScreenInteractionActionsMixin, ScreenFocusActionsMixin, Screen[None]): - """ - TUI 浏览型页面。 - - :param snapshot: 浏览页快照 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param navigation_items: 导航项列表 - :param refreshed_at: 本次刷新时间 - """ - - BINDINGS = [ - Binding('left', 'focus_left', TUI_COPY.build_internal_binding_label('focus_left'), show=False), - Binding('right', 'focus_right', TUI_COPY.build_internal_binding_label('focus_right'), show=False), - Binding('j', 'scroll_focus_down', TUI_COPY.build_internal_binding_label('scroll_down'), show=False), - Binding('k', 'scroll_focus_up', TUI_COPY.build_internal_binding_label('scroll_up'), show=False), - Binding('pagedown', 'scroll_focus_page_down', TUI_COPY.build_internal_binding_label('page_down'), show=False), - Binding('pageup', 'scroll_focus_page_up', TUI_COPY.build_internal_binding_label('page_up'), show=False), - Binding('home', 'scroll_focus_home', TUI_COPY.build_internal_binding_label('home'), show=False), - Binding('end', 'scroll_focus_end', TUI_COPY.build_internal_binding_label('end'), show=False), - Binding('x', 'trigger_primary_action', TUI_COPY.build_internal_binding_label('action_primary'), show=False), - Binding('z', 'trigger_secondary_action', TUI_COPY.build_internal_binding_label('action_secondary'), show=False), - Binding('y', 'trigger_global_action', TUI_COPY.build_internal_binding_label('action_global'), show=False), - Binding('w', 'trigger_utility_action', TUI_COPY.build_internal_binding_label('action_utility'), show=False), - Binding('1', 'apply_filter_1', TUI_COPY.build_internal_binding_label('filter_1'), show=False), - Binding('2', 'apply_filter_2', TUI_COPY.build_internal_binding_label('filter_2'), show=False), - Binding('3', 'apply_filter_3', TUI_COPY.build_internal_binding_label('filter_3'), show=False), - Binding('4', 'apply_filter_4', TUI_COPY.build_internal_binding_label('filter_4'), show=False), - Binding('/', 'open_search', TUI_COPY.build_internal_binding_label('search'), show=False), - Binding('backspace', 'clear_search', TUI_COPY.build_internal_binding_label('clear_search'), show=False), - ] - - def __init__( - self, - snapshot: BrowserPageSnapshot, - env: str, - active_view: str, - navigation_items: list[NavigationItem], - refreshed_at: str, - action_feedback_lines: list[str] | None = None, - support: BrowserScreenSupport | None = None, - focus_service: BrowserScreenFocusService | None = None, - ) -> None: - """ - 初始化浏览型页面。 - - :param snapshot: 浏览页快照 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param navigation_items: 导航项列表 - :param refreshed_at: 本次刷新时间 - :param action_feedback_lines: 最近动作反馈文本 - :param support: 浏览页屏幕支持对象 - :param focus_service: 浏览页焦点服务 - :return: None - """ - self.snapshot = snapshot - self.env = env - self.active_view = active_view - self.navigation_items = navigation_items - self.refreshed_at = refreshed_at - self.selected_record_index = 0 - self.selected_section_index = 0 - self._is_syncing_sections = False - self._record_detail_request_id = 0 - self._record_detail_task: asyncio.Task[None] | None = None - self._action_task: asyncio.Task[None] | None = None - self._last_action_result: TuiActionResult | None = None - self._action_feedback_lines = list(action_feedback_lines or []) - self.support = support or BrowserScreenSupport() - self.focus_service = focus_service or BrowserScreenFocusService() - super().__init__() - - def _build_summary_text(self) -> str: - """ - 构建浏览页摘要文本。 - - :return: 摘要文本 - """ - return self.support.build_summary_text(self.snapshot, self.active_view) - - def _build_filter_bar_text(self) -> str: - """ - 构建当前浏览页筛选条文本。 - - :return: 筛选条文本 - """ - return self.support.build_filter_bar_text(self.snapshot) - - def _current_search_query(self) -> str: - return self.support.current_search_query(self.snapshot) - - def _apply_filter_shortcut(self, shortcut: str) -> None: - """ - 按快捷键切换当前浏览页筛选条件。 - - :param shortcut: 筛选快捷键 - :return: None - """ - if not self.snapshot.filters: - return - target = next((option for option in self.snapshot.filters if option.shortcut == shortcut), None) - if target is None or target.key == self.snapshot.active_filter_key: - return - remember_filter = getattr(self.app, 'remember_browser_filter', None) - if callable(remember_filter): - remember_filter(self.active_view, target.key) - self.app.action_refresh_current_view() - - def _open_search(self) -> None: - """ - 打开当前浏览页搜索输入弹窗。 - - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.open_search_dialog( - self, - self.snapshot.search, - self._handle_search_submitted, - ) - - def _handle_search_submitted(self, query: str | None) -> None: - """ - 处理页内搜索输入结果。 - - :param query: 搜索词 - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.remember_query_and_refresh(self, self.active_view, query) - - def _clear_search(self) -> None: - """ - 清空当前浏览页搜索词。 - - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.clear_query_and_refresh( - self, - self.active_view, - self._current_search_query(), - ) - - def _resolve_action(self, slot: str) -> TuiActionSpec | None: - """ - 解析当前页面指定槽位对应的动作。 - - :param slot: 动作槽位 - :return: 动作定义 - """ - return self.support.resolve_action( - active_view=self.active_view, - slot=slot, - record=self._get_record_or_fallback(self.selected_record_index), - env=self.env, - ) - - def _build_record_action_lines(self, record: BrowserRecordSnapshot) -> list[str]: - """ - 构建当前记录动作面板文本。 - - :param record: 当前记录 - :return: 动作面板文本 - """ - return self.support.build_record_action_lines( - active_view=self.active_view, - record=record, - env=self.env, - feedback_lines=self._action_feedback_lines, - ) - - def _open_action_confirm(self, slot: str) -> None: - """ - 打开指定槽位动作的确认弹窗。 - - :param slot: 动作槽位 - :return: None - """ - self._action_task = TUI_SCREEN_INTERACTION_SERVICE.confirm_and_schedule_action( - self, - self._resolve_action(slot), - self._action_task, - self._execute_action, - ) - - async def _execute_action(self, action: TuiActionSpec) -> None: - """ - 执行低风险动作并在结束后通知与刷新页面。 - - :param action: 动作定义 - :return: None - """ - - async def _handle_result(result: TuiActionResult, feedback_lines: list[str]) -> None: - self._last_action_result = result - self._action_feedback_lines = feedback_lines - await self._render_record_detail(eager=False) - - await TUI_SCREEN_INTERACTION_SERVICE.execute_action_with_feedback( - self, - action, - self.env, - self.active_view, - _handle_result, - ) - - def _get_record_or_fallback(self, index: int) -> BrowserRecordSnapshot: - """ - 获取指定索引的记录快照,缺失时返回兜底记录。 - - :param index: 记录索引 - :return: 记录快照 - """ - if 0 <= index < len(self.snapshot.records): - return self.snapshot.records[index] - return self.support.build_empty_record() - - def _build_loading_section(self) -> DetailSectionSnapshot: - """ - 构建详情加载中的占位分区。 - - :return: 占位分区 - """ - return self.support.build_loading_section() - - def _build_detail_load_failure_sections(self, error: Exception) -> list[DetailSectionSnapshot]: - """ - 构建详情加载失败时的兜底分区。 - - :param error: 后台加载异常 - :return: 失败分区列表 - """ - resource_name = self.snapshot.title if '详情' in self.snapshot.title else f'{self.snapshot.title}详情' - return self.support.build_detail_load_failure_sections(error, resource_name) - - @staticmethod - def _cancel_task(task: asyncio.Task[None] | None) -> None: - """ - 取消指定后台任务。 - - :param task: 待取消任务 - :return: None - """ - if task is None or task.done(): - return - task.cancel() - - def _cancel_background_tasks(self) -> None: - """ - 取消当前屏幕持有的后台任务。 - - :return: None - """ - self._record_detail_request_id += 1 - self._cancel_task(self._record_detail_task) - self._cancel_task(self._action_task) - - def _get_sections_for_record(self, index: int, *, eager: bool = False) -> list[DetailSectionSnapshot]: - """ - 获取指定记录对应的详情分区列表。 - - :param index: 记录索引 - :param eager: 是否立即加载按需详情 - :return: 详情分区列表 - """ - record = self._get_record_or_fallback(index) - if eager: - try: - detail_sections = record.resolve_detail_sections() - except Exception as error: - detail_sections = self._build_detail_load_failure_sections(error) - elif record.detail_sections: - detail_sections = record.detail_sections - elif record._cached_detail_sections: - detail_sections = list(record._cached_detail_sections) - elif record.detail_loader is not None: - detail_sections = [self._build_loading_section()] - else: - detail_sections = [] - return [*detail_sections, *self.snapshot.shared_sections] - - def _get_section_or_fallback(self, record_index: int, section_index: int) -> DetailSectionSnapshot: - """ - 获取指定记录下的分区快照,缺失时返回兜底分区。 - - :param record_index: 记录索引 - :param section_index: 分区索引 - :return: 分区快照 - """ - sections = self._get_sections_for_record(record_index) - if 0 <= section_index < len(sections): - return sections[section_index] - return self.support.build_empty_section() - - async def _render_record_detail(self, *, eager: bool = False) -> None: - """ - 根据当前选中记录刷新右侧详情区域。 - - :param eager: 是否立即解析完整详情 - :return: None - """ - record = self._get_record_or_fallback(self.selected_record_index) - sections = self._get_sections_for_record(self.selected_record_index, eager=eager) - selected_section = self._get_section_or_fallback(self.selected_record_index, self.selected_section_index) - self.query_one(RecordSummaryView).show_record( - record, - selected_section, - self._build_record_action_lines(record), - self._current_search_query(), - ) - navigator = self.query_one(SectionNavigator) - detail_view = self.query_one(SectionDetailView) - self._is_syncing_sections = True - try: - await navigator.show_sections( - sections, - initial_index=self.selected_section_index, - query=self._current_search_query(), - ) - detail_view.show_section(selected_section, query=self._current_search_query()) - self._sync_detail_pane_state(selected_section) - finally: - self._is_syncing_sections = False - - def _sync_detail_pane_state(self, section: DetailSectionSnapshot) -> None: - """ - 依据当前分区状态同步右侧详情容器样式。 - - :param section: 当前分区快照 - :return: None - """ - detail_pane = self.query_one('#browser-detail-pane', Vertical) - detail_pane.remove_class('is-ok', 'is-fail', 'is-warn', 'is-info') - normalized = section.status if section.status in {'ok', 'fail', 'warn'} else 'info' - detail_pane.add_class(f'is-{normalized}') - - async def _load_record_detail_async(self, index: int, request_id: int) -> None: - """ - 在后台线程中加载指定记录的详情,并在仍为当前选中记录时回填界面。 - - :param index: 记录索引 - :param request_id: 当前加载请求编号 - :return: None - """ - record = self._get_record_or_fallback(index) - try: - await asyncio.to_thread(record.resolve_detail_sections) - except Exception as error: - failure_sections = self._build_detail_load_failure_sections(error) - object.__setattr__(record, '_cached_detail_sections', tuple(failure_sections)) - if request_id != self._record_detail_request_id: - return - if index != self.selected_record_index: - return - await self._render_record_detail(eager=False) - - def _schedule_record_detail_load(self, index: int) -> None: - """ - 为当前选中记录安排一次后台详情加载。 - - :param index: 记录索引 - :return: None - """ - record = self._get_record_or_fallback(index) - if record.detail_loader is None: - return - if record._cached_detail_sections: - return - self._cancel_task(self._record_detail_task) - self._record_detail_request_id += 1 - self._record_detail_task = asyncio.create_task( - self._load_record_detail_async(index, self._record_detail_request_id) - ) - - async def _update_selected_record(self, index: int) -> None: - """ - 更新当前选中记录,并同步右侧详情视图。 - - :param index: 待选中的记录索引 - :return: None - """ - if not self.snapshot.records: - self.selected_record_index = 0 - self.selected_section_index = 0 - await self._render_record_detail(eager=False) - return - if not 0 <= index < len(self.snapshot.records): - return - if index == self.selected_record_index: - return - self.selected_record_index = index - self.selected_section_index = 0 - await self._render_record_detail(eager=False) - self._schedule_record_detail_load(index) - - def _update_selected_section(self, index: int) -> None: - """ - 更新当前选中分区,并同步最右侧详情内容。 - - :param index: 分区索引 - :return: None - """ - if self._is_syncing_sections: - return - sections = self._get_sections_for_record(self.selected_record_index) - if not sections: - detail_view = self.query_one(SectionDetailView) - fallback = self._get_section_or_fallback(self.selected_record_index, index) - detail_view.show_section(fallback, query=self._current_search_query()) - record = self._get_record_or_fallback(self.selected_record_index) - self.query_one(RecordSummaryView).show_record( - record, - fallback, - self._build_record_action_lines(record), - self._current_search_query(), - ) - self._sync_detail_pane_state(fallback) - return - if not 0 <= index < len(sections): - return - if index == self.selected_section_index: - return - self.selected_section_index = index - current_section = sections[index] - detail_view = self.query_one(SectionDetailView) - detail_view.show_section(current_section, query=self._current_search_query()) - record = self._get_record_or_fallback(self.selected_record_index) - self.query_one(RecordSummaryView).show_record( - record, - current_section, - self._build_record_action_lines(record), - self._current_search_query(), - ) - self._sync_detail_pane_state(current_section) - - def on_mount(self) -> None: - """ - 浏览页挂载后默认将焦点停留在左侧导航。 - - :return: None - """ - self.call_after_refresh(self.query_one(WorkspaceSidebar).focus) - self._schedule_record_detail_load(self.selected_record_index) - - def on_unmount(self) -> None: - """ - 浏览页卸载时取消尚未完成的后台任务。 - - :return: None - """ - self._cancel_background_tasks() - - def action_apply_filter_1(self) -> None: - """ - 应用第一个浏览页筛选条件。 - - :return: None - """ - self._apply_filter_shortcut('1') - - def action_apply_filter_2(self) -> None: - """ - 应用第二个浏览页筛选条件。 - - :return: None - """ - self._apply_filter_shortcut('2') - - def action_apply_filter_3(self) -> None: - """ - 应用第三个浏览页筛选条件。 - - :return: None - """ - self._apply_filter_shortcut('3') - - def action_apply_filter_4(self) -> None: - """ - 应用第四个浏览页筛选条件。 - - :return: None - """ - self._apply_filter_shortcut('4') - - def _open_sidebar_item(self, event: WorkspaceSidebar.Highlighted | WorkspaceSidebar.Selected) -> None: - """ - 根据侧边栏事件打开对应视图,并避免重复打开当前页面。 - - :param event: 侧边栏高亮或选中事件 - :return: None - """ - item = getattr(event.item, 'item', None) - if not isinstance(item, NavigationItem): - return - if item.view_key == self.active_view: - return - self.app.open_view(item.view_key) - - @on(WorkspaceSidebar.Selected) - def on_sidebar_selected(self, event: WorkspaceSidebar.Selected) -> None: - """ - 响应左侧导航选中事件并切换视图。 - - :param event: 导航选中事件 - :return: None - """ - self._open_sidebar_item(event) - - @on(WorkspaceSidebar.Highlighted) - def on_sidebar_highlighted(self, event: WorkspaceSidebar.Highlighted) -> None: - """ - 响应左侧导航高亮变更并立即切换视图。 - - :param event: 导航高亮事件 - :return: None - """ - self._open_sidebar_item(event) - - def compose(self) -> ComposeResult: - """ - 构建浏览页界面结构。 - - :return: Textual 组件结果 - """ - yield WorkspaceHeader(self.env, self.active_view) - with Horizontal(id='workspace-shell'): - yield WorkspaceSidebar(self.env, self.navigation_items, self.active_view) - with ScrollableContainer(id='workspace-main'): - yield WorkspaceHero( - title=self.snapshot.title, - subtitle=self.snapshot.subtitle, - env=self.env, - active_view=self.active_view, - summary=self._build_summary_text(), - refreshed_at=self.refreshed_at, - ) - if self.snapshot.filters: - yield Static(self._build_filter_bar_text(), id='browser-filter-bar', markup=False) - with Horizontal(id='browser-body'): - yield RecordNavigator( - self.snapshot.records, - initial_index=self.selected_record_index, - query=self._current_search_query(), - ) - with Vertical(id='browser-detail-pane'): - yield RecordSummaryView(self._get_record_or_fallback(self.selected_record_index)) - with Horizontal(id='browser-section-body'): - yield SectionNavigator( - self._get_sections_for_record(self.selected_record_index, eager=False), - initial_index=self.selected_section_index, - query=self._current_search_query(), - ) - yield SectionDetailView( - self._get_section_or_fallback( - self.selected_record_index, - self.selected_section_index, - ), - query=self._current_search_query(), - ) - yield Footer() - - @on(RecordNavigator.Changed) - async def on_record_changed(self, event: RecordNavigator.Changed) -> None: - """ - 响应记录导航高亮变更并更新详情内容。 - - :param event: 记录导航高亮事件 - :return: None - """ - await self._update_selected_record(event.index) - - @on(SectionNavigator.Changed) - def on_section_changed(self, event: SectionNavigator.Changed) -> None: - """ - 响应记录详情分区高亮变更并更新最右侧详情内容。 - - :param event: 分区导航高亮事件 - :return: None - """ - self._update_selected_section(event.index) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/confirm.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/confirm.py deleted file mode 100644 index abdc64c..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/confirm.py +++ /dev/null @@ -1,128 +0,0 @@ -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, Vertical -from textual.screen import ModalScreen -from textual.widgets import Button, Static - -from cli.tui.copy import TUI_COPY - - -class ActionConfirmScreen(ModalScreen[bool]): - """ - 通用动作确认弹窗。 - - :param title: 弹窗标题 - :param lines: 预览文本行 - :param confirm_label: 确认按钮文本 - """ - - CSS = """ - ActionConfirmScreen { - align: center middle; - } - - #action-confirm-dialog { - width: 96; - max-width: 96; - border: heavy #38d8ff; - background: #06111b; - padding: 1 2; - } - - #action-confirm-title { - text-style: bold; - color: #e9fcff; - margin-bottom: 1; - } - - #action-confirm-body { - border: round #18425d; - background: #081827; - color: #d8f7ff; - padding: 1 2; - margin-bottom: 1; - } - - #action-confirm-actions { - height: auto; - align-horizontal: right; - } - - #action-confirm-hint { - color: #8fc6d6; - margin-bottom: 1; - } - - #action-confirm-cancel { - margin-right: 1; - } - """ - - BINDINGS = [ - Binding('escape', 'cancel', TUI_COPY.build_confirm_binding_label('cancel'), show=False), - Binding('enter', 'submit', TUI_COPY.build_confirm_binding_label('submit'), show=False), - ] - - def __init__(self, title: str, lines: list[str], confirm_label: str) -> None: - super().__init__() - self._dialog_title = title - self._dialog_lines = lines - self._confirm_label = confirm_label - - def compose(self) -> ComposeResult: - """ - 构建确认弹窗界面。 - - :return: Textual 组件结果 - """ - with Vertical(id='action-confirm-dialog'): - yield Static(self._dialog_title, id='action-confirm-title', markup=False) - yield Static('\n'.join(self._dialog_lines), id='action-confirm-body', markup=False) - yield Static(TUI_COPY.build_action_confirm_hint(), id='action-confirm-hint', markup=False) - with Horizontal(id='action-confirm-actions'): - yield Button(TUI_COPY.build_action_confirm_cancel_label(), id='action-confirm-cancel') - yield Button(self._confirm_label, id='action-confirm-submit', variant='primary') - - def on_mount(self) -> None: - """ - 弹窗挂载后默认聚焦确认按钮。 - - :return: None - """ - self.call_after_refresh(self.query_one('#action-confirm-submit', Button).focus) - - def action_cancel(self) -> None: - """ - 取消并关闭弹窗。 - - :return: None - """ - self.dismiss(False) - - def action_submit(self) -> None: - """ - 通过键盘确认执行当前动作。 - - :return: None - """ - submit_button = self.query_one('#action-confirm-submit', Button) - submit_button.press() - - @on(Button.Pressed, '#action-confirm-cancel') - def on_cancel_pressed(self) -> None: - """ - 响应取消按钮。 - - :return: None - """ - self.dismiss(False) - - @on(Button.Pressed, '#action-confirm-submit') - def on_submit_pressed(self) -> None: - """ - 响应确认按钮。 - - :return: None - """ - self.dismiss(True) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/dashboard.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/dashboard.py deleted file mode 100644 index ae8b460..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/dashboard.py +++ /dev/null @@ -1,253 +0,0 @@ -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Grid, Horizontal, ScrollableContainer -from textual.screen import Screen -from textual.widgets import Footer - -from cli.tui.adapters import DashboardSnapshot -from cli.tui.copy import TUI_COPY -from cli.tui.keymaps import TUI_KEYMAP_REGISTRY -from cli.tui.screens.focus import BaseScreenFocusService, ScreenFocusActionsMixin -from cli.tui.screens.summary import STATUS_SUMMARY_BUILDER -from cli.tui.widgets import ( - MetricPanel, - NavigationItem, - SignalRail, - StatusPanel, - WorkspaceHeader, - WorkspaceHero, - WorkspaceSidebar, -) - -PRIMARY_METRIC_COUNT = 2 - - -class DashboardScreenSupport: - """ - 首页屏幕支持对象。 - - 该对象负责首页摘要文本、信号带文本和状态轨迹构建, - 使 `DashboardScreen` 本体主要保留 Textual 生命周期与界面编排。 - """ - - @staticmethod - def build_summary_text(snapshot: DashboardSnapshot) -> str: - """ - 构建首页摘要文本。 - - :param snapshot: 首页聚合快照 - :return: 摘要文本 - """ - summary = STATUS_SUMMARY_BUILDER.build(snapshot.panels) - return TUI_COPY.build_dashboard_summary_text( - total_count=summary.total_count, - ok_count=summary.ok_count, - warn_count=summary.warn_count, - fail_count=summary.fail_count, - ) - - @staticmethod - def build_status_track(snapshot: DashboardSnapshot) -> str: - """ - 构建首页面板状态轨迹字符串。 - - :param snapshot: 首页聚合快照 - :return: 状态轨迹 - """ - return ''.join( - 'x' if panel.status == 'fail' else '!' if panel.status == 'warn' else 'o' if panel.status == 'ok' else '-' - for panel in snapshot.panels - ) - - def build_signal_lines(self, snapshot: DashboardSnapshot) -> list[str]: - """ - 构建首页顶部信号带文本。 - - :param snapshot: 首页聚合快照 - :return: 信号文本行 - """ - summary = STATUS_SUMMARY_BUILDER.build(snapshot.panels) - return TUI_COPY.build_dashboard_signal_lines( - status_track=self.build_status_track(snapshot), - fail_count=summary.fail_count, - warn_count=summary.warn_count, - ok_count=summary.ok_count, - total_count=summary.total_count, - navigation_shortcut_hint=TUI_KEYMAP_REGISTRY.navigation_shortcut_hint, - ) - - -class DashboardScreenFocusService(BaseScreenFocusService): - """ - 首页焦点服务。 - - 该对象负责定义首页可聚焦组件顺序,以及按左右键和滚动键解析 - 当前目标区域。 - """ - - def get_focus_order(self, screen: 'DashboardScreen') -> list[WorkspaceSidebar | ScrollableContainer]: - """ - 定义首页可通过左右键切换的焦点顺序。 - - :param screen: 当前首页屏幕 - :return: 焦点顺序列表 - """ - return [ - screen.query_one(WorkspaceSidebar), - screen.query_one('#workspace-main', ScrollableContainer), - ] - - def get_default_scroll_target(self, screen: 'DashboardScreen') -> ScrollableContainer: - """ - 返回首页默认滚动容器。 - - :param screen: 当前首页屏幕 - :return: 主工作区滚动容器 - """ - return screen.query_one('#workspace-main', ScrollableContainer) - - -class DashboardScreen(ScreenFocusActionsMixin, Screen[None]): - """ - TUI 首页巡检屏幕。 - - :param snapshot: 首页聚合快照 - """ - - BINDINGS = [ - Binding('left', 'focus_left', TUI_COPY.build_internal_binding_label('focus_left'), show=False), - Binding('right', 'focus_right', TUI_COPY.build_internal_binding_label('focus_right'), show=False), - Binding('j', 'scroll_focus_down', TUI_COPY.build_internal_binding_label('scroll_down'), show=False), - Binding('k', 'scroll_focus_up', TUI_COPY.build_internal_binding_label('scroll_up'), show=False), - Binding('pagedown', 'scroll_focus_page_down', TUI_COPY.build_internal_binding_label('page_down'), show=False), - Binding('pageup', 'scroll_focus_page_up', TUI_COPY.build_internal_binding_label('page_up'), show=False), - Binding('home', 'scroll_focus_home', TUI_COPY.build_internal_binding_label('home'), show=False), - Binding('end', 'scroll_focus_end', TUI_COPY.build_internal_binding_label('end'), show=False), - ] - - def __init__( - self, - snapshot: DashboardSnapshot, - env: str, - active_view: str, - navigation_items: list[NavigationItem], - refreshed_at: str, - support: DashboardScreenSupport | None = None, - focus_service: DashboardScreenFocusService | None = None, - ) -> None: - """ - 初始化首页巡检屏幕。 - - :param snapshot: 首页聚合快照 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param navigation_items: 导航项列表 - :param refreshed_at: 本次刷新时间 - :param support: 首页屏幕支持对象 - :param focus_service: 首页焦点服务 - :return: None - """ - self.snapshot = snapshot - self.env = env - self.active_view = active_view - self.navigation_items = navigation_items - self.refreshed_at = refreshed_at - self.support = support or DashboardScreenSupport() - self.focus_service = focus_service or DashboardScreenFocusService() - super().__init__() - - def _build_summary_text(self) -> str: - """ - 构建首页摘要文本。 - - :return: 摘要文本 - """ - return self.support.build_summary_text(self.snapshot) - - def _build_signal_lines(self) -> list[str]: - """ - 构建首页顶部信号带文本。 - - :return: 信号文本行 - """ - return self.support.build_signal_lines(self.snapshot) - - def compose(self) -> ComposeResult: - """ - 构建首页界面结构。 - - :return: Textual 组件结果 - """ - yield WorkspaceHeader(self.env, self.active_view) - with Horizontal(id='workspace-shell'): - yield WorkspaceSidebar(self.env, self.navigation_items, self.active_view) - with ScrollableContainer(id='workspace-main'): - yield WorkspaceHero( - title=TUI_COPY.build_dashboard_hero_title(), - subtitle=TUI_COPY.build_dashboard_hero_subtitle(), - env=self.env, - active_view=self.active_view, - summary=self._build_summary_text(), - refreshed_at=self.refreshed_at, - ) - yield SignalRail(self._build_signal_lines()) - with Grid(id='dashboard-metrics'): - for index, metric in enumerate(self.snapshot.metrics): - yield MetricPanel( - title=metric.title, - value=metric.value, - status=metric.status, - hint=metric.hint, - accent=index < PRIMARY_METRIC_COUNT, - ) - with Grid(id='dashboard-grid'): - for panel in self.snapshot.panels: - yield StatusPanel( - title=panel.title, - status=panel.status, - body='\n'.join(panel.lines), - ) - yield Footer() - - def on_mount(self) -> None: - """ - 首页挂载后默认将焦点停留在左侧导航。 - - :return: None - """ - self.call_after_refresh(self.query_one(WorkspaceSidebar).focus) - - def _open_sidebar_item(self, event: WorkspaceSidebar.Highlighted | WorkspaceSidebar.Selected) -> None: - """ - 根据侧边栏事件打开对应视图,并避免重复打开当前页面。 - - :param event: 侧边栏高亮或选中事件 - :return: None - """ - item = getattr(event.item, 'item', None) - if not isinstance(item, NavigationItem): - return - if item.view_key == self.active_view: - return - self.app.open_view(item.view_key) - - @on(WorkspaceSidebar.Selected) - def on_sidebar_selected(self, event: WorkspaceSidebar.Selected) -> None: - """ - 响应左侧导航选中事件并切换视图。 - - :param event: 导航选中事件 - :return: None - """ - self._open_sidebar_item(event) - - @on(WorkspaceSidebar.Highlighted) - def on_sidebar_highlighted(self, event: WorkspaceSidebar.Highlighted) -> None: - """ - 响应左侧导航高亮变更并立即切换视图。 - - :param event: 导航高亮事件 - :return: None - """ - self._open_sidebar_item(event) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/detail.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/detail.py deleted file mode 100644 index 97e2c69..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/detail.py +++ /dev/null @@ -1,413 +0,0 @@ -import asyncio - -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, ScrollableContainer -from textual.screen import Screen -from textual.widgets import Footer - -from cli.tui.actions import ( - TUI_ACTION_PRESENTATION_SERVICE, - TUI_ACTION_REGISTRY, - TuiActionResult, - TuiActionSpec, -) -from cli.tui.adapters import DetailPageSnapshot, DetailSectionSnapshot -from cli.tui.adapters.models import TUI_ADAPTER_MODEL_RENDERER -from cli.tui.copy import TUI_COPY -from cli.tui.screens.focus import BaseScreenFocusService, ScreenFocusActionsMixin -from cli.tui.screens.interactions import TUI_SCREEN_INTERACTION_SERVICE, ScreenInteractionActionsMixin -from cli.tui.screens.summary import STATUS_SUMMARY_BUILDER -from cli.tui.widgets import ( - NavigationItem, - SectionDetailView, - SectionNavigator, - WorkspaceHeader, - WorkspaceHero, - WorkspaceSidebar, -) - - -class DetailScreenSupport: - """ - 详情页屏幕支持对象。 - - 该对象负责详情页摘要、搜索词、动作解析与空态分区构建, - 使 `DetailScreen` 本体主要保留 Textual 事件桥接。 - """ - - @staticmethod - def build_empty_section() -> DetailSectionSnapshot: - """ - 构建详情页空态兜底分区。 - - :return: 分区快照 - """ - return DetailSectionSnapshot( - title=TUI_COPY.build_detail_empty_section_copy('title'), - status='info', - lines=TUI_ADAPTER_MODEL_RENDERER.build_empty_lines( - empty_label=TUI_COPY.build_detail_empty_section_copy('label'), - empty_value=TUI_COPY.build_detail_empty_section_copy('value'), - detail=TUI_COPY.build_detail_empty_section_copy('detail_page'), - ), - ) - - @staticmethod - def build_summary_text(snapshot: DetailPageSnapshot, active_view: str) -> str: - """ - 构建详情页摘要文本。 - - :param snapshot: 页面快照 - :param active_view: 当前激活视图 - :return: 摘要文本 - """ - summary = STATUS_SUMMARY_BUILDER.build(snapshot.sections) - return ( - TUI_COPY.build_detail_summary_text( - total_count=summary.total_count, - ok_count=summary.ok_count, - warn_count=summary.warn_count, - fail_count=summary.fail_count, - ) - + f' {TUI_ACTION_PRESENTATION_SERVICE.build_detail_action_hint(active_view)}' - ) - - @staticmethod - def current_search_query(snapshot: DetailPageSnapshot) -> str: - """ - 读取当前详情页搜索词。 - - :param snapshot: 页面快照 - :return: 搜索词 - """ - search_context = snapshot.search - return search_context.query if search_context is not None else '' - - @staticmethod - def resolve_action(*, active_view: str, slot: str, env: str) -> TuiActionSpec | None: - """ - 解析当前详情页指定槽位对应的动作。 - - :param active_view: 当前激活视图 - :param slot: 动作槽位 - :param env: 当前运行环境 - :return: 动作定义 - """ - return TUI_ACTION_REGISTRY.resolve_detail_action( - view_key=active_view, - slot=slot, # type: ignore[arg-type] - env=env, - ) - - -class DetailScreenFocusService(BaseScreenFocusService): - """ - 详情页焦点服务。 - - 该对象负责定义详情页可聚焦组件顺序,以及按左右键和滚动键解析 - 当前目标区域。 - """ - - def get_focus_order(self, screen: 'DetailScreen') -> list[WorkspaceSidebar | SectionNavigator | SectionDetailView]: - """ - 定义详情页可通过左右键切换的焦点顺序。 - - :param screen: 当前详情页屏幕 - :return: 焦点顺序列表 - """ - return [ - screen.query_one(WorkspaceSidebar), - screen.query_one(SectionNavigator), - screen.query_one(SectionDetailView), - ] - - def get_default_scroll_target( - self, - screen: 'DetailScreen', - ) -> ScrollableContainer: - """ - 返回详情页默认滚动容器。 - - :param screen: 当前详情页屏幕 - :return: 主工作区滚动容器 - """ - return screen.query_one('#workspace-main', ScrollableContainer) - - -class DetailScreen(ScreenInteractionActionsMixin, ScreenFocusActionsMixin, Screen[None]): - """ - TUI 通用详情屏幕。 - - :param snapshot: 页面快照 - """ - - BINDINGS = [ - Binding('left', 'focus_left', TUI_COPY.build_internal_binding_label('focus_left'), show=False), - Binding('right', 'focus_right', TUI_COPY.build_internal_binding_label('focus_right'), show=False), - Binding('j', 'scroll_focus_down', TUI_COPY.build_internal_binding_label('scroll_down'), show=False), - Binding('k', 'scroll_focus_up', TUI_COPY.build_internal_binding_label('scroll_up'), show=False), - Binding('pagedown', 'scroll_focus_page_down', TUI_COPY.build_internal_binding_label('page_down'), show=False), - Binding('pageup', 'scroll_focus_page_up', TUI_COPY.build_internal_binding_label('page_up'), show=False), - Binding('home', 'scroll_focus_home', TUI_COPY.build_internal_binding_label('home'), show=False), - Binding('end', 'scroll_focus_end', TUI_COPY.build_internal_binding_label('end'), show=False), - Binding('x', 'trigger_primary_action', TUI_COPY.build_internal_binding_label('action_primary'), show=False), - Binding('z', 'trigger_secondary_action', TUI_COPY.build_internal_binding_label('action_secondary'), show=False), - Binding('y', 'trigger_global_action', TUI_COPY.build_internal_binding_label('action_global'), show=False), - Binding('w', 'trigger_utility_action', TUI_COPY.build_internal_binding_label('action_utility'), show=False), - Binding('/', 'open_search', TUI_COPY.build_internal_binding_label('search'), show=False), - Binding('backspace', 'clear_search', TUI_COPY.build_internal_binding_label('clear_search'), show=False), - ] - - def __init__( - self, - snapshot: DetailPageSnapshot, - env: str, - active_view: str, - navigation_items: list[NavigationItem], - refreshed_at: str, - support: DetailScreenSupport | None = None, - focus_service: DetailScreenFocusService | None = None, - ) -> None: - """ - 初始化通用详情屏幕。 - - :param snapshot: 页面快照 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param navigation_items: 导航项列表 - :param refreshed_at: 本次刷新时间 - :param support: 详情页屏幕支持对象 - :param focus_service: 详情页焦点服务 - :return: None - """ - self.snapshot = snapshot - self.env = env - self.active_view = active_view - self.navigation_items = navigation_items - self.refreshed_at = refreshed_at - self.selected_section_index = 0 - self._last_action_result: TuiActionResult | None = None - self._action_feedback_lines: list[str] = [] - self._action_task: asyncio.Task[None] | None = None - self.support = support or DetailScreenSupport() - self.focus_service = focus_service or DetailScreenFocusService() - super().__init__() - - @staticmethod - def _cancel_task(task: asyncio.Task[None] | None) -> None: - """ - 取消指定后台任务。 - - :param task: 待取消任务 - :return: None - """ - if task is None or task.done(): - return - task.cancel() - - def _get_section_or_fallback(self, index: int) -> DetailSectionSnapshot: - """ - 获取指定索引的分区快照,缺失时返回兜底分区。 - - :param index: 分区索引 - :return: 分区快照 - """ - if 0 <= index < len(self.snapshot.sections): - return self.snapshot.sections[index] - return self.support.build_empty_section() - - def _build_summary_text(self) -> str: - """ - 构建详情页摘要文本。 - - :return: 摘要文本 - """ - return self.support.build_summary_text(self.snapshot, self.active_view) - - def _current_search_query(self) -> str: - return self.support.current_search_query(self.snapshot) - - def _open_search(self) -> None: - """ - 打开当前详情页搜索输入弹窗。 - - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.open_search_dialog( - self, - self.snapshot.search, - self._handle_search_submitted, - ) - - def _handle_search_submitted(self, query: str | None) -> None: - """ - 处理详情页搜索输入结果。 - - :param query: 搜索词 - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.remember_query_and_refresh(self, self.active_view, query) - - def _clear_search(self) -> None: - """ - 清空当前详情页搜索词。 - - :return: None - """ - TUI_SCREEN_INTERACTION_SERVICE.clear_query_and_refresh( - self, - self.active_view, - self._current_search_query(), - ) - - def _resolve_action(self, slot: str) -> TuiActionSpec | None: - """ - 解析当前详情页指定槽位对应的动作。 - - :param slot: 动作槽位 - :return: 动作定义 - """ - return self.support.resolve_action(active_view=self.active_view, slot=slot, env=self.env) - - def _open_action_confirm(self, slot: str) -> None: - """ - 打开指定槽位动作的确认弹窗。 - - :param slot: 动作槽位 - :return: None - """ - self._action_task = TUI_SCREEN_INTERACTION_SERVICE.confirm_and_schedule_action( - self, - self._resolve_action(slot), - self._action_task, - self._execute_action, - ) - - async def _execute_action(self, action: TuiActionSpec) -> None: - """ - 执行详情页动作。 - - :param action: 动作定义 - :return: None - """ - - def _handle_result(result: TuiActionResult, feedback_lines: list[str]) -> None: - self._last_action_result = result - self._action_feedback_lines = feedback_lines - - await TUI_SCREEN_INTERACTION_SERVICE.execute_action_with_feedback( - self, - action, - self.env, - self.active_view, - _handle_result, - ) - - def _update_selected_section(self, index: int) -> None: - """ - 更新当前选中分区,并同步右侧详情视图。 - - :param index: 待选中的分区索引 - :return: None - """ - if not self.snapshot.sections: - detail_view = self.query_one(SectionDetailView) - detail_view.show_section(self._get_section_or_fallback(index), query=self._current_search_query()) - return - if not 0 <= index < len(self.snapshot.sections): - return - self.selected_section_index = index - detail_view = self.query_one(SectionDetailView) - detail_view.show_section(self.snapshot.sections[index], query=self._current_search_query()) - - def compose(self) -> ComposeResult: - """ - 构建详情页界面结构。 - - :return: Textual 组件结果 - """ - yield WorkspaceHeader(self.env, self.active_view) - with Horizontal(id='workspace-shell'): - yield WorkspaceSidebar(self.env, self.navigation_items, self.active_view) - with ScrollableContainer(id='workspace-main'): - yield WorkspaceHero( - title=self.snapshot.title, - subtitle=self.snapshot.subtitle, - env=self.env, - active_view=self.active_view, - summary=self._build_summary_text(), - refreshed_at=self.refreshed_at, - ) - with Horizontal(id='detail-body'): - yield SectionNavigator( - self.snapshot.sections, - initial_index=self.selected_section_index, - query=self._current_search_query(), - ) - yield SectionDetailView( - self._get_section_or_fallback(self.selected_section_index), - query=self._current_search_query(), - ) - yield Footer() - - def on_mount(self) -> None: - """ - 详情页挂载后默认将焦点停留在左侧导航。 - - :return: None - """ - self.call_after_refresh(self.query_one(WorkspaceSidebar).focus) - - def on_unmount(self) -> None: - """ - 详情页卸载时取消尚未完成的后台任务。 - - :return: None - """ - self._cancel_task(self._action_task) - - def _open_sidebar_item(self, event: WorkspaceSidebar.Highlighted | WorkspaceSidebar.Selected) -> None: - """ - 根据侧边栏事件打开对应视图,并避免重复打开当前页面。 - - :param event: 侧边栏高亮或选中事件 - :return: None - """ - item = getattr(event.item, 'item', None) - if not isinstance(item, NavigationItem): - return - if item.view_key == self.active_view: - return - self.app.open_view(item.view_key) - - @on(WorkspaceSidebar.Selected) - def on_sidebar_selected(self, event: WorkspaceSidebar.Selected) -> None: - """ - 响应左侧导航选中事件并切换视图。 - - :param event: 导航选中事件 - :return: None - """ - self._open_sidebar_item(event) - - @on(WorkspaceSidebar.Highlighted) - def on_sidebar_highlighted(self, event: WorkspaceSidebar.Highlighted) -> None: - """ - 响应左侧导航高亮变更并立即切换视图。 - - :param event: 导航高亮事件 - :return: None - """ - self._open_sidebar_item(event) - - @on(SectionNavigator.Changed) - def on_section_changed(self, event: SectionNavigator.Changed) -> None: - """ - 响应分区导航高亮变更并更新详情内容。 - - :param event: 分区导航高亮事件 - :return: None - """ - self._update_selected_section(event.index) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/focus.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/focus.py deleted file mode 100644 index fa3a00e..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/focus.py +++ /dev/null @@ -1,152 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any - -from textual.containers import ScrollableContainer - - -class BaseScreenFocusService(ABC): - """ - TUI 屏幕焦点服务基类。 - - 该基类统一处理焦点顺序中的左右移动和滚动目标回退逻辑,具体页面 - 只需声明自身的焦点顺序和默认滚动容器。 - """ - - @abstractmethod - def get_focus_order(self, screen: Any) -> list[Any]: - """ - 返回当前页面的焦点顺序。 - - :param screen: 当前屏幕对象 - :return: 焦点组件列表 - """ - - @abstractmethod - def get_default_scroll_target(self, screen: Any) -> ScrollableContainer: - """ - 返回当前页面的默认滚动容器。 - - :param screen: 当前屏幕对象 - :return: 默认滚动容器 - """ - - def move_focus(self, screen: Any, step: int) -> None: - """ - 按给定方向移动当前焦点。 - - :param screen: 当前屏幕对象 - :param step: 焦点移动步长 - :return: None - """ - order = self.get_focus_order(screen) - focused = screen.app.focused - if focused not in order: - order[0].focus() - return - target_index = max(0, min(order.index(focused) + step, len(order) - 1)) - order[target_index].focus() - - def get_scroll_target(self, screen: Any) -> Any: - """ - 获取当前应响应滚动动作的焦点区域。 - - :param screen: 当前屏幕对象 - :return: 当前焦点组件或默认滚动容器 - """ - focused = screen.app.focused - order = self.get_focus_order(screen) - if focused in order: - return focused - return self.get_default_scroll_target(screen) - - -class ScreenFocusActionsMixin: - """ - TUI 屏幕焦点动作混入。 - - 该混入负责将焦点服务暴露为屏幕上的左右移动与滚动 action,避免 - 各个 Screen 类重复编写同形转发方法。 - """ - - focus_service: BaseScreenFocusService - - def _move_focus(self, step: int) -> None: - """ - 按给定方向移动当前焦点。 - - :param step: 焦点移动步长 - :return: None - """ - self.focus_service.move_focus(self, step) - - def _get_scroll_target(self) -> Any: - """ - 获取当前应响应滚动动作的焦点区域。 - - :return: 当前焦点组件或主工作区 - """ - return self.focus_service.get_scroll_target(self) - - def action_scroll_focus_down(self) -> None: - """ - 向下滚动当前焦点区域。 - - :return: None - """ - self._get_scroll_target().scroll_down(animate=False) - - def action_scroll_focus_up(self) -> None: - """ - 向上滚动当前焦点区域。 - - :return: None - """ - self._get_scroll_target().scroll_up(animate=False) - - def action_scroll_focus_page_down(self) -> None: - """ - 向下翻动当前焦点区域。 - - :return: None - """ - self._get_scroll_target().scroll_page_down(animate=False) - - def action_scroll_focus_page_up(self) -> None: - """ - 向上翻动当前焦点区域。 - - :return: None - """ - self._get_scroll_target().scroll_page_up(animate=False) - - def action_scroll_focus_home(self) -> None: - """ - 将当前焦点区域滚动到起始位置。 - - :return: None - """ - self._get_scroll_target().scroll_home(animate=False) - - def action_scroll_focus_end(self) -> None: - """ - 将当前焦点区域滚动到末尾位置。 - - :return: None - """ - self._get_scroll_target().scroll_end(animate=False) - - def action_focus_left(self) -> None: - """ - 将焦点向左移动一列。 - - :return: None - """ - self._move_focus(-1) - - def action_focus_right(self) -> None: - """ - 将焦点向右移动一列。 - - :return: None - """ - self._move_focus(1) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/interactions.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/interactions.py deleted file mode 100644 index 208469a..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/interactions.py +++ /dev/null @@ -1,331 +0,0 @@ -import asyncio -import inspect -from collections.abc import Awaitable, Callable -from typing import Any - -from textual.app import SuspendNotSupported - -from cli.tui.actions import ( - TUI_ACTION_EXECUTION_SERVICE, - TUI_ACTION_PRESENTATION_SERVICE, - TuiActionResult, - TuiActionSpec, -) -from cli.tui.copy import TUI_COPY -from cli.tui.screens.confirm import ActionConfirmScreen -from cli.tui.screens.search import SearchInputScreen -from cli.tui.search import PageSearchContext -from cli.tui.widgets import WorkspaceSidebar - - -class TuiScreenInteractionService: - """ - TUI 屏幕交互服务。 - - 该服务负责搜索弹窗、搜索状态写回、动作确认弹窗和动作执行通知等 - 屏幕级交互流程,减少浏览页与详情页之间的重复控制逻辑。 - """ - - @staticmethod - def open_search_dialog( - screen: Any, - search_context: PageSearchContext | None, - callback: Callable[[str | None], None], - ) -> None: - """ - 打开搜索输入弹窗。 - - :param screen: 当前屏幕对象 - :param search_context: 搜索上下文 - :param callback: 提交搜索词后的回调 - :return: None - """ - if search_context is None: - return - screen.app.push_screen( - SearchInputScreen( - '页内搜索', - search_context.placeholder, - search_context.query, - search_context.suggestions, - ), - callback=callback, - ) - - @staticmethod - def remember_query_and_refresh(screen: Any, active_view: str, query: str | None) -> None: - """ - 记录当前页面搜索词并刷新页面。 - - :param screen: 当前屏幕对象 - :param active_view: 当前页面视图标识 - :param query: 搜索词 - :return: None - """ - if query is None: - return - remember_query = getattr(screen.app, 'remember_browser_query', None) - if callable(remember_query): - remember_query(active_view, query) - screen.app.action_refresh_current_view() - - def clear_query_and_refresh(self, screen: Any, active_view: str, current_query: str) -> None: - """ - 清空当前页面搜索词并刷新页面。 - - :param screen: 当前屏幕对象 - :param active_view: 当前页面视图标识 - :param current_query: 当前搜索词 - :return: None - """ - if not current_query: - return - self.remember_query_and_refresh(screen, active_view, '') - - @staticmethod - def open_action_confirm( - screen: Any, - action: TuiActionSpec | None, - callback: Callable[[bool | None, TuiActionSpec], None], - ) -> None: - """ - 打开动作确认弹窗。 - - :param screen: 当前屏幕对象 - :param action: 待执行动作 - :param callback: 确认回调 - :return: None - """ - if action is None: - screen.notify( - TUI_COPY.build_action_unavailable_message(), - title=TUI_COPY.build_action_notification_title(), - severity='warning', - ) - return - screen.app.push_screen( - ActionConfirmScreen(action.preview_title, action.preview_lines, action.label), - callback=lambda confirmed, action=action: callback(confirmed, action), - ) - - @staticmethod - def schedule_action_task( - existing_task: asyncio.Task[None] | None, - action: TuiActionSpec, - execute_callback: Callable[[TuiActionSpec], Awaitable[None]], - ) -> asyncio.Task[None]: - """ - 安排动作执行任务,并在必要时取消旧任务。 - - :param existing_task: 现有动作任务 - :param action: 待执行动作 - :param execute_callback: 动作执行协程工厂 - :return: 新建任务 - """ - if existing_task is not None and not existing_task.done(): - existing_task.cancel() - return asyncio.create_task(execute_callback(action)) - - def confirm_and_schedule_action( - self, - screen: Any, - action: TuiActionSpec | None, - existing_task: asyncio.Task[None] | None, - execute_callback: Callable[[TuiActionSpec], Awaitable[None]], - ) -> asyncio.Task[None] | None: - """ - 统一处理动作确认结果,并在确认后调度后台执行任务。 - - :param screen: 当前屏幕对象 - :param action: 待执行动作 - :param existing_task: 当前已有动作任务 - :param execute_callback: 动作执行协程工厂 - :return: 新建任务或原任务 - """ - if action is None: - self.open_action_confirm(screen, action, lambda confirmed, action_spec: None) - return existing_task - - scheduled_task = existing_task - - def _handle_confirmed(confirmed: bool | None, action_spec: TuiActionSpec) -> None: - nonlocal scheduled_task - if not confirmed: - return - scheduled_task = self.schedule_action_task( - scheduled_task, - action_spec, - execute_callback, - ) - - self.open_action_confirm(screen, action, _handle_confirmed) - return scheduled_task - - async def execute_action( - self, - screen: Any, - action: TuiActionSpec, - env: str, - active_view: str, - *, - on_result: Callable[[TuiActionResult, list[str]], Any] | None = None, - ) -> TuiActionResult: - """ - 执行屏幕动作并统一处理通知、反馈持久化与刷新。 - - :param screen: 当前屏幕对象 - :param action: 动作定义 - :param env: 当前运行环境 - :param active_view: 当前页面视图标识 - :param on_result: 动作结果回调 - :return: 动作结果 - """ - screen.notify( - TUI_COPY.build_action_running_message(action.label), - title=TUI_COPY.build_action_notification_title(), - ) - if action.execution_mode == 'external': - try: - with screen.app.suspend(): - result = TUI_ACTION_EXECUTION_SERVICE.execute_external(action) - except SuspendNotSupported: - result = TuiActionResult( - spec=action, - external_exit_code=1, - external_message='当前终端不支持挂起 TUI,无法打开外部交互向导', - ) - else: - result = await asyncio.to_thread(TUI_ACTION_EXECUTION_SERVICE.execute, action, env) - - feedback_lines = TUI_ACTION_EXECUTION_SERVICE.build_result_lines(result) - remember_feedback = getattr(screen.app, 'remember_action_feedback', None) - if callable(remember_feedback): - remember_feedback(active_view, feedback_lines) - if on_result is not None: - maybe_result = on_result(result, feedback_lines) - if inspect.isawaitable(maybe_result): - await maybe_result - screen.notify( - TUI_ACTION_PRESENTATION_SERVICE.build_action_result_message(result), - title=TUI_COPY.build_action_notification_title(), - severity='information' if result.ok else 'error', - ) - if result.ok and action.refresh_view: - screen.app.action_refresh_current_view() - return result - - async def execute_action_with_feedback( - self, - screen: Any, - action: TuiActionSpec, - env: str, - active_view: str, - feedback_callback: Callable[[TuiActionResult, list[str]], Any], - ) -> TuiActionResult: - """ - 执行动作并将统一反馈回调注入结果收口流程。 - - :param screen: 当前屏幕对象 - :param action: 动作定义 - :param env: 当前运行环境 - :param active_view: 当前页面视图标识 - :param feedback_callback: 反馈写回回调 - :return: 动作结果 - """ - return await self.execute_action( - screen, - action, - env, - active_view, - on_result=feedback_callback, - ) - - -class ScreenInteractionActionsMixin: - """ - 屏幕交互动作混入。 - - 该混入负责把搜索、动作确认和侧栏切屏这类重复桥接 action 收口为 - 通用 Screen 方法,具体页面只需实现对应的私有协作方法。 - """ - - def _open_search(self) -> None: - """ - 打开当前页面搜索输入弹窗。 - - :return: None - """ - - def _clear_search(self) -> None: - """ - 清空当前页面搜索词。 - - :return: None - """ - - def _open_action_confirm(self, slot: str) -> None: - """ - 打开指定槽位动作的确认弹窗。 - - :param slot: 动作槽位 - :return: None - """ - - def _open_sidebar_item(self, event: WorkspaceSidebar.Highlighted | WorkspaceSidebar.Selected) -> None: - """ - 根据侧栏事件打开对应视图。 - - :param event: 侧栏事件 - :return: None - """ - - def action_open_search(self) -> None: - """ - 打开当前页面搜索输入。 - - :return: None - """ - self._open_search() - - def action_clear_search(self) -> None: - """ - 清空当前页面搜索词。 - - :return: None - """ - self._clear_search() - - def action_trigger_primary_action(self) -> None: - """ - 触发当前页面主动作。 - - :return: None - """ - self._open_action_confirm('primary') - - def action_trigger_secondary_action(self) -> None: - """ - 触发当前页面次动作。 - - :return: None - """ - self._open_action_confirm('secondary') - - def action_trigger_global_action(self) -> None: - """ - 触发当前页面全局动作。 - - :return: None - """ - self._open_action_confirm('global') - - def action_trigger_utility_action(self) -> None: - """ - 触发当前页面工具动作。 - - :return: None - """ - self._open_action_confirm('utility') - - -TUI_SCREEN_INTERACTION_SERVICE = TuiScreenInteractionService() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/search.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/search.py deleted file mode 100644 index fac67be..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/search.py +++ /dev/null @@ -1,80 +0,0 @@ -from textual import on -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Vertical -from textual.screen import ModalScreen -from textual.widgets import Input, Static - -from cli.tui.copy import TUI_COPY - - -class SearchInputScreen(ModalScreen[str | None]): - """ - 通用搜索输入弹窗。 - - :param title: 弹窗标题 - :param placeholder: 输入框提示 - :param value: 当前搜索词 - :param suggestions: 候选建议 - """ - - CSS = """ - SearchInputScreen { - align: center middle; - } - - #search-dialog { - width: 88; - max-width: 88; - border: heavy #38d8ff; - background: #06111b; - padding: 1 2; - } - - #search-title { - text-style: bold; - color: #e9fcff; - margin-bottom: 1; - } - - #search-input { - margin-bottom: 1; - } - - #search-suggestions { - color: #8fc6d6; - } - """ - - BINDINGS = [ - Binding('escape', 'cancel', TUI_COPY.build_confirm_binding_label('cancel'), show=False), - Binding('enter', 'submit', TUI_COPY.build_confirm_binding_label('submit'), show=False), - ] - - def __init__(self, title: str, placeholder: str, value: str, suggestions: list[str]) -> None: - super().__init__() - self._title = title - self._placeholder = placeholder - self._value = value - self._suggestions = suggestions - - def compose(self) -> ComposeResult: - with Vertical(id='search-dialog'): - yield Static(self._title, id='search-title', markup=False) - yield Input(value=self._value, placeholder=self._placeholder, id='search-input') - suggestion_text = '候选建议 · ' + (' '.join(self._suggestions[:6]) if self._suggestions else '暂无建议') - yield Static(suggestion_text, id='search-suggestions', markup=False) - - def on_mount(self) -> None: - self.call_after_refresh(self.query_one('#search-input', Input).focus) - - def action_cancel(self) -> None: - self.dismiss(None) - - def action_submit(self) -> None: - input_widget = self.query_one('#search-input', Input) - self.dismiss(str(input_widget.value or '').strip()) - - @on(Input.Submitted, '#search-input') - def on_input_submitted(self) -> None: - self.action_submit() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/summary.py b/shierfeng-fastapi-backend/build/lib/cli/tui/screens/summary.py deleted file mode 100644 index f359cc8..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/screens/summary.py +++ /dev/null @@ -1,59 +0,0 @@ -from collections.abc import Iterable -from dataclasses import dataclass -from typing import Protocol - - -class StatusCarrier(Protocol): - """ - 状态载体协议。 - - 该协议用于描述首页面板、详情分区和浏览记录这类具备 `status` - 属性的摘要统计对象。 - """ - - status: str - - -@dataclass(frozen=True) -class StatusSummary: - """ - 状态摘要统计结果。 - - :param total_count: 总数量 - :param ok_count: 正常数量 - :param warn_count: 警告数量 - :param fail_count: 失败数量 - """ - - total_count: int - ok_count: int - warn_count: int - fail_count: int - - -class StatusSummaryBuilder: - """ - 状态摘要构建器。 - - 该对象负责对带 `status` 属性的集合做统一计数,供 dashboard、 - detail、browser 三类 screen support 复用。 - """ - - @staticmethod - def build(items: Iterable[StatusCarrier]) -> StatusSummary: - """ - 统计状态摘要。 - - :param items: 待统计对象集合 - :return: 状态摘要 - """ - materialized = list(items) - return StatusSummary( - total_count=len(materialized), - ok_count=sum(1 for item in materialized if item.status == 'ok'), - warn_count=sum(1 for item in materialized if item.status == 'warn'), - fail_count=sum(1 for item in materialized if item.status == 'fail'), - ) - - -STATUS_SUMMARY_BUILDER = StatusSummaryBuilder() diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/search.py b/shierfeng-fastapi-backend/build/lib/cli/tui/search.py deleted file mode 100644 index 9990549..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/search.py +++ /dev/null @@ -1,375 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass, field - -from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway - -CompletionProviderCallable = Callable[[object, list[str] | None, str], list[str]] - - -@dataclass(frozen=True) -class PageFilterOption: - """ - 浏览页筛选项定义。 - - :param key: 筛选键 - :param label: 展示名称 - :param shortcut: 快捷键 - """ - - key: str - label: str - shortcut: str - - -@dataclass(frozen=True) -class PageSearchContext: - """ - 浏览页搜索上下文定义。 - - :param placeholder: 搜索输入提示 - :param query: 当前搜索词 - :param suggestions: 候选建议列表 - """ - - placeholder: str - query: str = '' - suggestions: list[str] = field(default_factory=list) - - -@dataclass(frozen=True) -class SearchSuggestionProviderSpec: - """ - 页面搜索建议提供者定义。 - - :param placeholder: 搜索输入提示 - :param suggestion_provider: 候选建议提供函数 - """ - - placeholder: str - suggestion_provider: Callable[[str], list[str]] | None = None - - -JOB_FILTER_OPTIONS: tuple[PageFilterOption, ...] = ( - PageFilterOption('all', '全部', '1'), - PageFilterOption('failed', '失败', '2'), - PageFilterOption('paused', '暂停', '3'), - PageFilterOption('ok', '正常', '4'), -) - -CONFIG_FILTER_OPTIONS: tuple[PageFilterOption, ...] = ( - PageFilterOption('all', '全部', '1'), - PageFilterOption('risky', '高风险', '2'), - PageFilterOption('mismatch', '值不一致', '3'), - PageFilterOption('cache-drift', '缓存异常', '4'), -) - - -class TuiSearchSuggestionProviderRegistry: - """ - TUI 搜索建议提供者注册表。 - - 该对象负责管理页面搜索提示文案及其候选建议提供函数,避免搜索服务 - 继续通过字符串反射隐式定位 completion provider。 - - :param providers: 页面搜索提供者映射 - """ - - def __init__(self, providers: dict[str, SearchSuggestionProviderSpec]) -> None: - """ - 初始化搜索建议提供者注册表。 - - :param providers: 页面搜索提供者映射 - :return: None - """ - self.providers = {self.normalize_view_key(key): value for key, value in providers.items()} - - @staticmethod - def normalize_view_key(view_key: str) -> str: - """ - 规范化页面视图标识。 - - :param view_key: 原始页面视图标识 - :return: 规范化后的页面视图标识 - """ - return str(view_key or '').strip().lower() - - def get_provider(self, view_key: str) -> SearchSuggestionProviderSpec | None: - """ - 获取指定页面的搜索建议提供者。 - - :param view_key: 页面视图标识 - :return: 搜索建议提供者定义 - """ - return self.providers.get(self.normalize_view_key(view_key)) - - -class CompletionSuggestionProviderFactory: - """ - 基于 completion 注册表构建 TUI 搜索建议提供函数。 - - :param completion_provider_gateway: completion 提供器对外网关 - """ - - def __init__(self, completion_provider_gateway: CompletionProviderGateway) -> None: - """ - 初始化 completion 搜索建议工厂。 - - :param completion_provider_gateway: completion 提供器对外网关 - :return: None - """ - self.completion_provider_gateway = completion_provider_gateway - - def build(self, provider: CompletionProviderCallable) -> Callable[[str], list[str]]: - """ - 根据显式 completion provider 构建建议提供函数。 - - :param provider: completion provider 可调用对象 - :return: 搜索建议提供函数 - """ - - def provide(query: str) -> list[str]: - """ - 调用 completion provider 生成候选建议。 - - :param query: 当前搜索词 - :return: 候选建议列表 - """ - candidates = provider(None, [], str(query or '').strip()) - return [str(candidate).strip() for candidate in candidates if str(candidate).strip()] - - return provide - - -def build_default_search_provider_registry( - completion_provider_gateway: CompletionProviderGateway, -) -> TuiSearchSuggestionProviderRegistry: - """ - 构建默认 TUI 搜索建议提供者注册表。 - - :param completion_provider_gateway: completion 提供器对外网关 - :return: 搜索建议提供者注册表 - """ - provider_factory = CompletionSuggestionProviderFactory(completion_provider_gateway) - return TuiSearchSuggestionProviderRegistry( - providers={ - 'jobs': SearchSuggestionProviderSpec( - '按任务名搜索', - provider_factory.build(completion_provider_gateway.complete_job_names), - ), - 'configs': SearchSuggestionProviderSpec( - '按配置键搜索', - provider_factory.build(completion_provider_gateway.complete_config_keys), - ), - 'cache': SearchSuggestionProviderSpec( - '按缓存名搜索', - provider_factory.build(completion_provider_gateway.complete_cache_names), - ), - 'gen': SearchSuggestionProviderSpec( - '按业务表名搜索', - provider_factory.build(completion_provider_gateway.complete_gen_table_names), - ), - 'database': SearchSuggestionProviderSpec( - '按 revision 搜索', - provider_factory.build(completion_provider_gateway.complete_alembic_revisions), - ), - 'app': SearchSuggestionProviderSpec('按分区或内容搜索'), - 'ops': SearchSuggestionProviderSpec('按分区或内容搜索'), - 'crypto': SearchSuggestionProviderSpec('按分区或内容搜索'), - } - ) - - -class TuiSearchHighlighter: - """ - TUI 搜索高亮器。 - - 该对象负责对终端文本做轻量关键字高亮。 - """ - - @staticmethod - def highlight(text: str, query: str, *, left_tag: str = '【', right_tag: str = '】') -> str: - """ - 对给定文本中的搜索词做轻量高亮。 - - :param text: 原始文本 - :param query: 搜索词 - :param left_tag: 左高亮标记 - :param right_tag: 右高亮标记 - :return: 高亮后的文本 - """ - raw_text = str(text or '') - normalized_query = str(query or '').strip() - if not raw_text or not normalized_query: - return raw_text - lower_text = raw_text.lower() - lower_query = normalized_query.lower() - result: list[str] = [] - cursor = 0 - query_length = len(normalized_query) - while True: - index = lower_text.find(lower_query, cursor) - if index < 0: - result.append(raw_text[cursor:]) - break - result.append(raw_text[cursor:index]) - result.append(f'{left_tag}{raw_text[index : index + query_length]}{right_tag}') - cursor = index + query_length - return ''.join(result) - - -class TuiSearchService: - """ - TUI 搜索服务。 - - 该对象负责筛选项解析、筛选条文案构建、搜索上下文装配和详情分区过滤。 - """ - - def __init__(self, provider_registry: TuiSearchSuggestionProviderRegistry) -> None: - """ - 初始化 TUI 搜索服务。 - - :param provider_registry: 页面搜索建议提供者注册表 - :return: None - """ - self.provider_registry = provider_registry - - @staticmethod - def resolve_filter_option( - options: list[PageFilterOption] | tuple[PageFilterOption, ...], - filter_key: str | None, - ) -> PageFilterOption | None: - """ - 从筛选项列表中解析指定键对应的筛选定义。 - - :param options: 筛选项列表 - :param filter_key: 筛选键 - :return: 匹配到的筛选项 - """ - normalized_key = str(filter_key or '').strip().lower() - if not normalized_key: - return None - for option in options: - if option.key == normalized_key: - return option - return None - - def build_filter_bar_text( - self, - options: list[PageFilterOption] | tuple[PageFilterOption, ...], - active_filter_key: str | None, - *, - search_query: str | None = None, - search_placeholder: str | None = None, - search_suggestions: list[str] | None = None, - ) -> str: - """ - 构建浏览页顶部筛选条文本。 - - :param options: 筛选项列表 - :param active_filter_key: 当前激活筛选键 - :param search_query: 当前搜索词 - :param search_placeholder: 搜索提示文本 - :param search_suggestions: 搜索候选建议 - :return: 筛选条文本 - """ - if not options and not search_placeholder: - return '' - lines: list[str] = [] - normalized_active = str(active_filter_key or '').strip().lower() - if options: - rendered_items = [ - f'[{option.shortcut}] {option.label}{" *" if option.key == normalized_active else ""}' - for option in options - ] - active_option = self.resolve_filter_option(options, normalized_active) - active_label = active_option.label if active_option is not None else '未指定' - lines.extend( - [ - f'筛选器 · {" ".join(rendered_items)}', - f'当前筛选 · {active_label}', - ] - ) - if search_placeholder: - normalized_query = str(search_query or '').strip() - lines.append(f'搜索器 · [/] {search_placeholder}') - lines.append(f'当前搜索 · {normalized_query or "未指定"} | [Backspace] 清空') - if search_suggestions: - lines.append(f'候选建议 · {" ".join(search_suggestions[:4])}') - return '\n'.join(lines) - - def resolve_search_context( - self, - view_key: str, - query: str = '', - *, - default_suggestions: list[str] | None = None, - ) -> PageSearchContext | None: - """ - 根据页面视图解析搜索上下文,并复用 completion provider 生成候选建议。 - - :param view_key: 当前页面视图标识 - :param query: 当前搜索词 - :param default_suggestions: 缺省候选建议 - :return: 搜索上下文 - """ - normalized_view = str(view_key).strip().lower() - provider_spec = self.provider_registry.get_provider(normalized_view) - if provider_spec is None: - return None - suggestions = self.resolve_search_suggestions(provider_spec, query) - if not suggestions and default_suggestions: - normalized_query = str(query or '').strip().lower() - suggestions = [ - suggestion - for suggestion in default_suggestions - if suggestion and (not normalized_query or normalized_query in suggestion.lower()) - ] - return PageSearchContext( - placeholder=provider_spec.placeholder, - query=str(query or '').strip(), - suggestions=suggestions[:6], - ) - - @staticmethod - def resolve_search_suggestions(provider_spec: SearchSuggestionProviderSpec, query: str) -> list[str]: - """ - 调用搜索建议提供函数生成当前搜索词的候选建议。 - - :param provider_spec: 搜索建议提供者定义 - :param query: 当前搜索词 - :return: 候选建议列表 - """ - normalized_query = str(query or '').strip() - if not normalized_query or not callable(provider_spec.suggestion_provider): - return [] - try: - candidates = provider_spec.suggestion_provider(normalized_query) - except Exception: - return [] - return [str(candidate).strip() for candidate in candidates if str(candidate).strip()] - - @staticmethod - def filter_detail_sections(sections: list[object], query: str) -> list[object]: - """ - 按搜索词过滤详情页分区列表。 - - :param sections: 原始分区列表 - :param query: 当前搜索词 - :return: 过滤后的分区列表 - """ - normalized_query = str(query or '').strip().lower() - if not normalized_query: - return sections - filtered_sections: list[object] = [] - for section in sections: - title = str(getattr(section, 'title', '') or '').strip().lower() - lines = getattr(section, 'lines', []) - joined_lines = ' '.join(str(line).strip().lower() for line in lines if str(line).strip()) - if normalized_query in title or normalized_query in joined_lines: - filtered_sections.append(section) - return filtered_sections - - -TUI_SEARCH_HIGHLIGHTER = TuiSearchHighlighter() -TUI_SEARCH_PROVIDER_REGISTRY = build_default_search_provider_registry(COMPLETION_PROVIDER_GATEWAY) -TUI_SEARCH_SERVICE = TuiSearchService(TUI_SEARCH_PROVIDER_REGISTRY) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/__init__.py deleted file mode 100644 index d51fc91..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -from cli.tui.widgets.status_panel import MetricPanel, SignalRail, StatusPanel -from cli.tui.widgets.workspace import ( - NavigationItem, - RecordDetailView, - RecordNavigator, - RecordSummaryView, - SectionDetailView, - SectionNavigator, - WorkspaceHeader, - WorkspaceHero, - WorkspaceSidebar, -) - -__all__ = [ - 'MetricPanel', - 'NavigationItem', - 'RecordDetailView', - 'RecordNavigator', - 'RecordSummaryView', - 'SectionDetailView', - 'SectionNavigator', - 'SignalRail', - 'StatusPanel', - 'WorkspaceHeader', - 'WorkspaceHero', - 'WorkspaceSidebar', -] diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/status_panel.py b/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/status_panel.py deleted file mode 100644 index 6e31ea9..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/status_panel.py +++ /dev/null @@ -1,323 +0,0 @@ -from textual.widgets import Static - -from cli.tui.copy import TUI_COPY - - -class StatusPanelRenderingSupport: - """ - 状态面板渲染支持对象。 - - 该对象负责状态类名解析、结构化正文渲染和信号带脉冲文本构建, - 供状态面板、指标卡与信号带组件复用。 - """ - - PULSE_FRAMES = ['·', '•', '◉', '•'] - - @staticmethod - def resolve_status_class(status: str) -> str: - """ - 根据状态值解析样式类。 - - :param status: 面板状态 - :return: 状态样式类名 - """ - normalized_status = str(status).strip().lower() - if normalized_status in {'ok', 'success', 'healthy'}: - return 'is-ok' - if normalized_status in {'fail', 'error', 'down'}: - return 'is-fail' - if normalized_status in {'warn', 'warning', 'degraded'}: - return 'is-warn' - return 'is-info' - - def render_structured_body(self, body: str) -> str: - """ - 将结构化正文文本渲染为更有层次的终端块内容。 - - 支持以下约定: - - `## ` 开头:分组标题 - - `> ` 开头:说明或子项 - - 空字符串:保留空行 - - :param body: 原始正文文本 - :return: 渲染后的正文文本 - """ - rendered_lines: list[str] = [] - for raw_line in body.splitlines(): - stripped = raw_line.strip() - if not stripped: - if rendered_lines and rendered_lines[-1] != '': - rendered_lines.append('') - continue - if stripped.startswith('## '): - title = stripped[3:].strip() - if rendered_lines and rendered_lines[-1] != '': - rendered_lines.append('') - rendered_lines.append(f'【{title}】') - rendered_lines.append('─' * max(10, min(len(title) * 2, 28))) - continue - if stripped.startswith('> '): - rendered_lines.append(f'│ {stripped[2:].strip()}') - continue - rendered_lines.append(f'• {stripped}') - - while rendered_lines and rendered_lines[0] == '': - rendered_lines.pop(0) - while rendered_lines and rendered_lines[-1] == '': - rendered_lines.pop() - return '\n'.join(rendered_lines) if rendered_lines else f'- {TUI_COPY.build_status_panel_empty_text()}' - - def build_signal_rail_text(self, lines: list[str], pulse_index: int) -> str: - """ - 构建信号带渲染文本。 - - :param lines: 信号文本行 - :param pulse_index: 当前脉冲帧索引 - :return: 渲染文本 - """ - pulse = self.PULSE_FRAMES[pulse_index % len(self.PULSE_FRAMES)] - body = '\n'.join(line for line in lines if line.strip()) - return TUI_COPY.build_signal_rail_text(pulse, body or TUI_COPY.build_signal_rail_empty_text()) - - -STATUS_PANEL_RENDERING = StatusPanelRenderingSupport() - - -class StatusPanel(Static): - """ - TUI 状态面板组件。 - - :param title: 面板标题 - :param status: 面板状态 - :param body: 面板正文 - """ - - DEFAULT_CSS = """ - StatusPanel { - border: double #245874; - padding: 1 2; - margin: 0; - height: 17; - min-height: 17; - background: #08131f; - color: #def8ff; - } - - StatusPanel.is-ok { - border: double #22c983; - background: #081b17; - } - - StatusPanel.is-fail { - border: double #ff6b7a; - background: #241118; - } - - StatusPanel.is-warn { - border: double #f2b84b; - background: #261d0f; - } - - StatusPanel.is-info { - border: double #38d8ff; - background: #081825; - } - - StatusPanel:focus { - border: heavy #6df7ff; - background: #10364b; - } - """ - - def __init__( - self, - title: str, - status: str, - body: str, - rendering: StatusPanelRenderingSupport | None = None, - ) -> None: - """ - 初始化状态面板。 - - :param title: 面板标题 - :param status: 面板状态 - :param body: 面板正文 - :param rendering: 状态面板渲染支持对象 - :return: None - """ - self.title = title - self.status = status - self.body = body - self.rendering = rendering or STATUS_PANEL_RENDERING - super().__init__(self._build_render_text(), markup=False) - self.can_focus = True - self.add_class(self.rendering.resolve_status_class(self.status)) - - def _build_render_text(self) -> str: - """ - 构建面板渲染文本。 - - :return: 渲染文本 - """ - rendered_body = self.rendering.render_structured_body(self.body) - normalized_status = TUI_COPY.render_status_label(self.status) - return TUI_COPY.build_status_panel_text( - self.title, - TUI_COPY.render_status_code(self.status), - normalized_status, - rendered_body, - ) - - -class MetricPanel(Static): - """ - TUI 首页驾驶舱指标卡组件。 - - :param title: 指标标题 - :param value: 指标主值 - :param status: 指标状态 - :param hint: 指标补充说明 - """ - - DEFAULT_CSS = """ - MetricPanel { - border: double #245874; - padding: 1 2; - height: 12; - min-height: 12; - background: #08131f; - color: #effcff; - } - - MetricPanel.is-primary { - border: heavy #6df7ff; - height: 12; - min-height: 12; - background: #0a1725; - text-style: bold; - } - - MetricPanel.is-ok { - border: double #22c983; - background: #081b17; - } - - MetricPanel.is-fail { - border: double #ff6b7a; - background: #241118; - } - - MetricPanel.is-warn { - border: double #f2b84b; - background: #261d0f; - } - - MetricPanel.is-info { - border: double #38d8ff; - background: #081825; - } - """ - - def __init__( - self, - title: str, - value: str, - status: str, - hint: str, - *, - accent: bool = False, - rendering: StatusPanelRenderingSupport | None = None, - ) -> None: - """ - 初始化指标卡。 - - :param title: 指标标题 - :param value: 指标主值 - :param status: 指标状态 - :param hint: 指标补充说明 - :param accent: 是否作为主指标高亮展示 - :param rendering: 状态面板渲染支持对象 - :return: None - """ - self.title = title - self.value = value - self.status = status - self.hint = hint - self.rendering = rendering or STATUS_PANEL_RENDERING - super().__init__(self._build_render_text(), markup=False) - self.add_class(self.rendering.resolve_status_class(self.status)) - if accent: - self.add_class('is-primary') - - def _build_render_text(self) -> str: - """ - 构建指标卡渲染文本。 - - :return: 渲染文本 - """ - normalized_status = TUI_COPY.render_status_label(self.status) - return TUI_COPY.build_metric_panel_text( - self.title, - self.value, - TUI_COPY.render_status_code(self.status), - normalized_status, - self.hint, - ) - - -class SignalRail(Static): - """ - 首页信号带组件,用于展示简洁的系统态势条目。 - - :param lines: 信号文本行 - """ - - DEFAULT_CSS = """ - SignalRail { - border: double #1db9d1; - padding: 1 2; - margin-top: 1; - margin-bottom: 1; - min-height: 6; - background: #06111b; - color: #e9fdff; - } - """ - - def __init__(self, lines: list[str], rendering: StatusPanelRenderingSupport | None = None) -> None: - """ - 初始化首页信号带。 - - :param lines: 信号文本行 - :param rendering: 状态面板渲染支持对象 - :return: None - """ - self.lines = lines - self._pulse_index = 0 - self.rendering = rendering or STATUS_PANEL_RENDERING - super().__init__(self._build_render_text(), markup=False) - - def on_mount(self) -> None: - """ - 挂载后启动轻量脉冲刷新。 - - :return: None - """ - self.set_interval(0.8, self._advance_pulse, name='signal rail pulse') - - def _advance_pulse(self) -> None: - """ - 推进信号带脉冲状态。 - - :return: None - """ - self._pulse_index = (self._pulse_index + 1) % 4 - self.update(self._build_render_text()) - - def _build_render_text(self) -> str: - """ - 构建信号带渲染文本。 - - :return: 渲染文本 - """ - return self.rendering.build_signal_rail_text(self.lines, self._pulse_index) diff --git a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/workspace.py b/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/workspace.py deleted file mode 100644 index 52ac4af..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/tui/widgets/workspace.py +++ /dev/null @@ -1,1262 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime -from math import sin -from time import monotonic - -from rich.text import Text -from textual.message import Message -from textual.widgets import Label, ListItem, ListView, Static - -from cli.tui.adapters.models import BrowserRecordSnapshot, DetailSectionSnapshot -from cli.tui.copy import TUI_COPY -from cli.tui.keymaps import TUI_KEYMAP_REGISTRY -from cli.tui.search import TUI_SEARCH_HIGHLIGHTER - - -class WorkspaceRenderingSupport: - """ - 工作台组件渲染支持对象。 - - 该对象负责状态徽标、状态类名、结构化文本渲染和预览提取, - 供工作台中的导航项、详情视图和摘要视图复用。 - """ - - @staticmethod - def highlight_search_text(text: str, query: str) -> str: - """ - 对工作台文本执行搜索高亮。 - - :param text: 原始文本 - :param query: 搜索词 - :return: 高亮后的文本 - """ - return TUI_SEARCH_HIGHLIGHTER.highlight(text, query) - - @staticmethod - def render_status_badge(status: str) -> str: - """ - 将状态值转换为紧凑徽标文本。 - - :param status: 原始状态值 - :return: 徽标文本 - """ - return f'[{TUI_COPY.render_status_code(status)}]' - - @staticmethod - def resolve_status_class(status: str) -> str: - """ - 根据状态值解析统一样式类。 - - :param status: 原始状态值 - :return: 状态样式类名 - """ - normalized_status = str(status).strip().lower() - if normalized_status in {'ok', 'success', 'healthy'}: - return 'is-ok' - if normalized_status in {'fail', 'error'}: - return 'is-fail' - if normalized_status in {'warn', 'warning'}: - return 'is-warn' - return 'is-info' - - @staticmethod - def strip_line_markup(line: str) -> str: - """ - 去除内部展示标记,便于提取预览文本。 - - :param line: 原始文本行 - :return: 纯文本内容 - """ - stripped = line.strip() - if stripped.startswith('## '): - return stripped[3:].strip() - if stripped.startswith('> '): - return stripped[2:].strip() - return stripped - - def build_preview_line(self, lines: list[str], fallback: str) -> str: - """ - 从结构化文本中提取首条适合作为预览的内容。 - - :param lines: 原始文本行列表 - :param fallback: 兜底文本 - :return: 预览文本 - """ - for line in lines: - preview = self.strip_line_markup(line) - if preview: - return preview - return fallback - - def render_structured_lines(self, lines: list[str], empty_text: str) -> str: - """ - 将结构化文本行渲染为适合终端展示的块文本。 - - 支持以下轻量约定: - - `## ` 开头:区块标题 - - `> ` 开头:说明或子项 - - 空字符串:保留空行 - - :param lines: 原始文本行列表 - :param empty_text: 空态提示文本 - :return: 渲染后的文本块 - """ - rendered_lines: list[str] = [] - for raw_line in lines: - stripped = raw_line.strip() - if not stripped: - if rendered_lines and rendered_lines[-1] != '': - rendered_lines.append('') - continue - if stripped.startswith('## '): - title = stripped[3:].strip() - if rendered_lines and rendered_lines[-1] != '': - rendered_lines.append('') - rendered_lines.append(f'【{title}】') - rendered_lines.append('─' * max(10, min(len(title) * 2, 30))) - continue - if stripped.startswith('> '): - rendered_lines.append(f'│ {stripped[2:].strip()}') - continue - rendered_lines.append(f'• {stripped}') - - while rendered_lines and rendered_lines[0] == '': - rendered_lines.pop(0) - while rendered_lines and rendered_lines[-1] == '': - rendered_lines.pop() - - if not rendered_lines: - return f'- {empty_text}' - return '\n'.join(rendered_lines) - - -WORKSPACE_RENDERING = WorkspaceRenderingSupport() - - -@dataclass(frozen=True) -class NavigationItem: - """ - 工作台导航项定义。 - - :param view_key: 视图唯一标识 - :param label: 导航显示名称 - :param shortcut: 快捷键 - :param description: 导航说明 - """ - - view_key: str - label: str - shortcut: str - description: str - - -class NavigationListItem(ListItem): - """ - 工作台导航列表项。 - - :param item: 导航项定义 - """ - - DEFAULT_CSS = """ - NavigationListItem { - padding: 0 1; - margin-bottom: 1; - border: round #18425d; - background: #07131f; - color: #89b9c9; - height: auto; - } - - NavigationListItem Label { - width: 1fr; - } - - NavigationListItem.is-active { - border: round #38d8ff; - background: #0a1e2d; - color: #e4fbff; - } - - NavigationListItem.is-active Label { - text-style: bold; - } - - NavigationListItem.-highlight { - border: heavy #6df7ff; - background: #10364b; - color: #ffffff; - text-style: bold; - padding-left: 2; - } - """ - - def __init__(self, item: NavigationItem, index: int, *, active: bool = False) -> None: - """ - 初始化导航列表项。 - - :param item: 导航项定义 - :return: None - """ - self.item = item - super().__init__( - Label( - '\n'.join(TUI_COPY.build_navigation_item_lines(index, item.label, item.shortcut, item.description)), - markup=False, - ) - ) - if active: - self.add_class('is-active') - - -class WorkspaceSidebar(ListView): - """ - 工作台左侧导航组件。 - - :param env: 当前运行环境 - :param items: 导航项列表 - :param active_view: 当前激活视图 - """ - - DEFAULT_CSS = """ - WorkspaceSidebar { - width: 34; - min-width: 34; - padding: 1; - margin: 1 0 1 1; - border: double #18425d; - background: #06111b; - } - - WorkspaceSidebar:focus { - border: heavy #6df7ff; - } - """ - - def __init__(self, env: str, items: list[NavigationItem], active_view: str) -> None: - """ - 初始化工作台导航组件。 - - :param env: 当前运行环境 - :param items: 导航项列表 - :param active_view: 当前激活视图 - :return: None - """ - self.env = env - self.items = items - self.active_view = active_view - initial_index = self._resolve_initial_index() - children = [ - NavigationListItem(item, index, active=item.view_key == self.active_view) - for index, item in enumerate(self.items) - ] - super().__init__(*children, initial_index=initial_index, id='workspace-sidebar') - - def _resolve_initial_index(self) -> int: - """ - 解析当前激活视图对应的导航索引。 - - :return: 初始高亮索引 - """ - for index, item in enumerate(self.items): - if item.view_key == self.active_view: - return index - return 0 - - -class WorkspaceHero(Static): - """ - 工作台顶部摘要组件。 - - :param title: 页面标题 - :param subtitle: 页面副标题 - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :param summary: 状态摘要文本 - :param refreshed_at: 本次刷新时间 - """ - - DEFAULT_CSS = """ - WorkspaceHero { - border: double #38d8ff; - padding: 1 2; - margin-bottom: 1; - min-height: 11; - background: #081827; - color: #e9fcff; - } - """ - _BORDER_PHASES = ('#2fb8df', '#38d8ff', '#62ecff', '#38d8ff') - _TITLE_GLOW_PALETTE = ('#7be8ff', '#b4f6ff', '#e9fdff', '#b4f6ff') - - def __init__( - self, - title: str, - subtitle: str, - env: str, - active_view: str, - summary: str, - refreshed_at: str, - ) -> None: - """ - 初始化工作台顶部摘要组件。 - - :param title: 页面标题 - :param subtitle: 页面副标题 - :param active_view: 当前激活视图 - :param summary: 状态摘要文本 - :param refreshed_at: 本次刷新时间 - :return: None - """ - self.title = title - self.subtitle = subtitle - self.env = env - self.active_view = active_view - self.summary = summary - self.refreshed_at = refreshed_at - super().__init__(self._build_render_text(), id='workspace-hero', markup=False) - - def on_mount(self) -> None: - """ - 在组件挂载后启动低频边框呼吸动效。 - - :return: None - """ - self.set_interval(0.9, self._pulse_border, name='pulse workspace hero border') - - def _pulse_border(self) -> None: - """ - 以低频节奏切换 Hero 边框颜色。 - - :return: None - """ - phase_index = int(monotonic() * 1.1) % len(self._BORDER_PHASES) - self.styles.border = ('double', self._BORDER_PHASES[phase_index]) - - @classmethod - def _build_title_text(cls, title: str) -> Text: - """ - 构建 Hero 标题渐变文本。 - - :param title: 标题文本 - :return: 富文本标题 - """ - rendered = Text() - palette_size = len(cls._TITLE_GLOW_PALETTE) - for index, char in enumerate(title): - rendered.append(char, style=f'bold {cls._TITLE_GLOW_PALETTE[index % palette_size]}') - return rendered - - def _build_render_text(self) -> Text: - """ - 构建顶部摘要文本。 - - :return: 渲染富文本 - """ - lines = TUI_COPY.build_workspace_hero_lines( - view_label=TUI_COPY.render_view_label(self.active_view), - title=self.title, - subtitle=self.subtitle, - env=self.env, - summary=self.summary, - refreshed_at=self.refreshed_at, - shortcut_hint=TUI_KEYMAP_REGISTRY.hero_shortcut_hint, - ) - rendered = Text() - for index, line in enumerate(lines): - if index: - rendered.append('\n') - if line.startswith('标题 · '): - rendered.append('标题 · ', style='bold #8fd9e8') - rendered.append_text(self._build_title_text(line.removeprefix('标题 · '))) - continue - rendered.append(line, style='bold #e9fcff' if index == 0 else '#d7f7ff') - return rendered - - -class WorkspaceHeader(Static): - """ - 工作台顶部状态栏。 - - :param env: 当前运行环境 - :param active_view: 当前激活视图 - """ - - DEFAULT_CSS = """ - WorkspaceHeader { - height: 2; - padding: 0 2; - background: #060d16; - color: #c7f6ff; - border-bottom: heavy #163c57; - content-align: left middle; - text-style: bold; - } - """ - _HEADER_LINE_COUNT = 2 - _SCANLINE_WIDTH = 16 - _HEADER_HORIZONTAL_PADDING = 4 - _HEADER_FALLBACK_WIDTH = 108 - _SCANLINE_BASE_STYLE = '#1c5368' - _SCANLINE_GLOW_STYLES = { - '·': '#8ef7ff', - '░': '#62ecff', - '▒': '#4ee4ff', - '▓': '#2cc9ff', - '█': '#dffcff', - } - - @classmethod - def build_scanline_text(cls, phase: float, width: int) -> Text: - """ - 构建页眉扫描线文本。 - - 通过单个高亮扫描头和两侧衰减尾迹,营造简洁的霓虹扫线效果。 - - :param phase: 当前动画相位 - :param width: 扫描线宽度 - :return: 带颜色层次的扫描线文本 - """ - cells = ['─'] * width - head_position = int(((sin(phase * 0.82) + 1) / 2) * (width - 1)) - for offset, symbol in ((-2, '·'), (-1, '░'), (0, '█'), (1, '▓'), (2, '▒')): - position = head_position + offset - if 0 <= position < width: - cells[position] = symbol - rendered = Text() - for symbol in cells: - rendered.append(symbol, style=cls._SCANLINE_GLOW_STYLES.get(symbol, cls._SCANLINE_BASE_STYLE)) - return rendered - - def __init__(self, env: str, active_view: str) -> None: - """ - 初始化工作台顶部状态栏。 - - :param env: 当前运行环境 - :param active_view: 当前激活视图 - :return: None - """ - self.env = env - self.active_view = active_view - super().__init__(id='workspace-header', markup=False) - - def on_mount(self) -> None: - """ - 在组件挂载后启动时钟刷新。 - - :return: None - """ - self.set_interval(0.18, self.refresh, name='update workspace header') - - def build_scanline_text_label(self) -> Text: - """ - 构建扫描线文本。 - - :return: 扫描线文本 - """ - phase = monotonic() * 2.1 - return self.build_scanline_text(phase, self._SCANLINE_WIDTH) - - def build_symmetric_effects(self) -> tuple[Text, Text]: - """ - 构建标题两侧的对称动效文本。 - - 左右两侧都使用固定宽度,确保标题中心点稳定,不随动效变化漂移。 - - :return: 左右动效文本 - """ - left_effect = self.build_scanline_text_label() - right_effect = Text() - for span_symbol in reversed(left_effect.plain): - right_effect.append( - span_symbol, - style=self._SCANLINE_GLOW_STYLES.get(span_symbol, self._SCANLINE_BASE_STYLE), - ) - return left_effect, right_effect - - def build_centered_title_line(self, title_text: str) -> Text: - """ - 构建居中的标题装饰行。 - - 标题左右分别放置镜像扫描线动效,中间保留标题主体。 - - :param title_text: 标题主体文本 - :return: 居中后的富文本标题行 - """ - left_effect, right_effect = self.build_symmetric_effects() - decorated_title = Text() - decorated_title.append_text(left_effect) - decorated_title.append(' ') - decorated_title.append(title_text, style='bold #e8fdff') - decorated_title.append(' ') - decorated_title.append_text(right_effect) - available_width = self.size.width - self._HEADER_HORIZONTAL_PADDING - if available_width <= 0: - available_width = self._HEADER_FALLBACK_WIDTH - centered = Text( - ' ' * max((max(available_width, len(decorated_title.plain)) - len(decorated_title.plain)) // 2, 0) - ) - centered.append_text(decorated_title) - return centered - - def render(self) -> Text: - """ - 渲染顶部状态栏文本。 - - :return: 渲染富文本 - """ - timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - lines = TUI_COPY.build_workspace_header_lines( - env=self.env, - view_label=TUI_COPY.render_view_label(self.active_view), - timestamp=timestamp, - shortcut_hint=TUI_KEYMAP_REGISTRY.global_shortcut_hint, - ) - rendered = Text() - if len(lines) >= self._HEADER_LINE_COUNT: - rendered.append_text(self.build_centered_title_line(lines[0])) - rendered.append('\n') - rendered.append(lines[1], style='bold #c7f6ff') - return rendered - rendered.append('\n'.join(lines), style='bold #c7f6ff') - return rendered - - -class SectionListItem(ListItem): - """ - 详情页分区导航列表项。 - - :param section: 分区快照 - :param index: 分区索引 - """ - - DEFAULT_CSS = """ - SectionListItem { - padding: 0 1; - margin-bottom: 1; - border: round #123148; - background: #07131f; - color: #a3c8d2; - height: auto; - } - - SectionListItem Label { - width: 1fr; - } - - SectionListItem.is-ok { - border: round #22c983; - background: #081b17; - } - - SectionListItem.is-fail { - border: round #ff6b7a; - background: #241118; - } - - SectionListItem.is-warn { - border: round #f2b84b; - background: #261d0f; - } - - SectionListItem.is-info { - border: round #38d8ff; - background: #081825; - } - - SectionListItem.-highlight { - border: heavy #6df7ff; - background: #10364b; - color: #ffffff; - text-style: bold; - padding-left: 2; - } - """ - - def __init__( - self, - section: DetailSectionSnapshot, - index: int, - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化详情页分区导航项。 - - :param section: 分区快照 - :param index: 分区索引 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.section = section - self.index = index - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - preview_line = self.rendering.build_preview_line(section.lines, TUI_COPY.build_workspace_empty_text('detail')) - super().__init__( - Label( - '\n'.join( - TUI_COPY.build_section_item_lines( - index, - self.rendering.highlight_search_text(section.title, query), - self.rendering.render_status_badge(section.status), - self.rendering.highlight_search_text(preview_line, query), - ) - ), - markup=False, - ) - ) - self.add_class(self.rendering.resolve_status_class(section.status)) - - -class SectionNavigator(ListView): - """ - 详情页分区导航组件。 - - :param sections: 分区快照列表 - :param initial_index: 初始高亮索引 - """ - - DEFAULT_CSS = """ - SectionNavigator { - width: 30; - min-width: 30; - border: double #18425d; - padding: 1; - margin-right: 1; - background: #06111b; - } - - SectionNavigator:focus { - border: heavy #6df7ff; - } - """ - - class Changed(Message): - """ - 分区导航高亮变更消息。 - """ - - def __init__(self, navigator: 'SectionNavigator', index: int, item: SectionListItem) -> None: - super().__init__() - self.navigator = navigator - self.index = index - self.item = item - - @property - def control(self) -> 'SectionNavigator': - return self.navigator - - def __init__( - self, - sections: list[DetailSectionSnapshot], - initial_index: int = 0, - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化详情页分区导航。 - - :param sections: 分区快照列表 - :param initial_index: 初始高亮索引 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.sections = sections - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - children = [ - SectionListItem(section, index, query=query, rendering=self.rendering) - for index, section in enumerate(sections) - ] - super().__init__(*children, initial_index=initial_index) - - def watch_index(self, old_index: int | None, new_index: int | None) -> None: - """ - 在高亮变化后向父级发送明确的分区切换消息。 - - :param old_index: 旧索引 - :param new_index: 新索引 - :return: None - """ - super().watch_index(old_index, new_index) - if new_index is None or not 0 <= new_index < len(self._nodes): - return - item = self._nodes[new_index] - if isinstance(item, SectionListItem): - self.post_message(self.Changed(self, new_index, item)) - - async def show_sections( - self, sections: list[DetailSectionSnapshot], initial_index: int = 0, query: str = '' - ) -> None: - """ - 刷新分区导航内容并恢复高亮位置。 - - :param sections: 最新分区快照列表 - :param initial_index: 目标高亮索引 - :return: None - """ - self.sections = sections - self.search_query = query - await self.clear() - children = [ - SectionListItem(section, index, query=query, rendering=self.rendering) - for index, section in enumerate(sections) - ] - if not children: - self.index = None - return - await self.extend(children) - self.index = max(0, min(initial_index, len(children) - 1)) - - -class SectionDetailView(Static): - """ - 详情页分区内容展示组件。 - - :param section: 当前展示的分区快照 - """ - - DEFAULT_CSS = """ - SectionDetailView { - border: double #245874; - padding: 1 2; - background: #08131f; - color: #ddf8ff; - width: 1fr; - min-height: 18; - overflow-x: auto; - overflow-y: auto; - } - - SectionDetailView.is-ok { - border: double #22c983; - background: #081b17; - } - - SectionDetailView.is-fail { - border: double #ff6b7a; - background: #241118; - } - - SectionDetailView.is-warn { - border: double #f2b84b; - background: #261d0f; - } - - SectionDetailView.is-info { - border: double #38d8ff; - background: #081825; - } - - SectionDetailView:focus { - border: heavy #6df7ff; - background: #10364b; - } - """ - - def __init__( - self, - section: DetailSectionSnapshot, - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化详情页分区内容展示组件。 - - :param section: 当前展示的分区快照 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.section = section - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - super().__init__(markup=False) - self.can_focus = True - self._render_section() - - def show_section(self, section: DetailSectionSnapshot, query: str = '') -> None: - """ - 切换并渲染当前展示的分区内容。 - - :param section: 待展示的分区快照 - :return: None - """ - self.section = section - self.search_query = query - self._render_section() - - def _render_section(self) -> None: - """ - 渲染当前分区内容。 - - :return: None - """ - self.remove_class('is-ok', 'is-fail', 'is-warn', 'is-info') - self.add_class(self.rendering.resolve_status_class(self.section.status)) - highlighted_lines = [ - self.rendering.highlight_search_text(line, self.search_query) for line in self.section.lines - ] - body = self.rendering.render_structured_lines( - highlighted_lines, - TUI_COPY.build_workspace_empty_text('detail'), - ) - self.update( - '\n'.join( - TUI_COPY.build_section_detail_lines( - self.rendering.highlight_search_text(self.section.title, self.search_query), - TUI_COPY.render_status_label(self.section.status), - self.rendering.render_status_badge(self.section.status), - body, - ) - ) - ) - - -class RecordListItem(ListItem): - """ - 浏览页记录导航列表项。 - - :param record: 记录快照 - :param index: 记录索引 - """ - - DEFAULT_CSS = """ - RecordListItem { - padding: 0 1; - margin-bottom: 1; - border: round #123148; - background: #07131f; - color: #a3c8d2; - height: auto; - } - - RecordListItem Label { - width: 1fr; - } - - RecordListItem.is-ok { - border: round #22c983; - background: #081b17; - } - - RecordListItem.is-fail { - border: round #ff6b7a; - background: #241118; - } - - RecordListItem.is-warn { - border: round #f2b84b; - background: #261d0f; - } - - RecordListItem.is-info { - border: round #38d8ff; - background: #081825; - } - - RecordListItem.-highlight { - border: heavy #6df7ff; - background: #10364b; - color: #ffffff; - text-style: bold; - padding-left: 2; - } - """ - - def __init__( - self, - record: BrowserRecordSnapshot, - index: int, - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化浏览页记录导航项。 - - :param record: 记录快照 - :param index: 记录索引 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.record = record - self.index = index - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - super().__init__( - Label( - '\n'.join( - TUI_COPY.build_record_item_lines( - index, - self.rendering.highlight_search_text(record.title, query), - self.rendering.render_status_badge(record.status), - self.rendering.highlight_search_text(record.summary, query), - ) - ), - markup=False, - ) - ) - self.add_class(self.rendering.resolve_status_class(record.status)) - - -class RecordNavigator(ListView): - """ - 浏览页记录导航组件。 - - :param records: 记录快照列表 - :param initial_index: 初始高亮索引 - """ - - DEFAULT_CSS = """ - RecordNavigator { - width: 34; - min-width: 34; - border: double #18425d; - padding: 1; - margin-right: 1; - background: #06111b; - } - - RecordNavigator:focus { - border: heavy #6df7ff; - } - """ - - class Changed(Message): - """ - 记录导航高亮变更消息。 - """ - - def __init__(self, navigator: 'RecordNavigator', index: int, item: RecordListItem) -> None: - super().__init__() - self.navigator = navigator - self.index = index - self.item = item - - @property - def control(self) -> 'RecordNavigator': - return self.navigator - - def __init__( - self, - records: list[BrowserRecordSnapshot], - initial_index: int = 0, - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化浏览页记录导航。 - - :param records: 记录快照列表 - :param initial_index: 初始高亮索引 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.records = records - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - children = [ - RecordListItem(record, index, query=query, rendering=self.rendering) for index, record in enumerate(records) - ] - super().__init__(*children, initial_index=initial_index, id='record-navigator') - - def watch_index(self, old_index: int | None, new_index: int | None) -> None: - """ - 在高亮变化后向父级发送明确的记录切换消息。 - - :param old_index: 旧索引 - :param new_index: 新索引 - :return: None - """ - super().watch_index(old_index, new_index) - if new_index is None or not 0 <= new_index < len(self._nodes): - return - item = self._nodes[new_index] - if isinstance(item, RecordListItem): - self.post_message(self.Changed(self, new_index, item)) - - -class RecordDetailView(Static): - """ - 浏览页记录详情展示组件。 - - :param record: 当前展示的记录快照 - :param shared_sections: 页面共享分区列表 - """ - - DEFAULT_CSS = """ - RecordDetailView { - border: double #245874; - padding: 1 2; - background: #08131f; - color: #ddf8ff; - width: 1fr; - min-height: 18; - overflow-x: auto; - overflow-y: auto; - } - - RecordDetailView.is-ok { - border: double #22c983; - } - - RecordDetailView.is-fail { - border: double #ff6b7a; - } - - RecordDetailView.is-warn { - border: double #f2b84b; - } - - RecordDetailView.is-info { - border: double #38d8ff; - } - - RecordDetailView:focus { - border: heavy #6df7ff; - background: #10364b; - } - """ - - def __init__( - self, - record: BrowserRecordSnapshot, - shared_sections: list[DetailSectionSnapshot], - query: str = '', - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化浏览页记录详情展示组件。 - - :param record: 当前展示的记录快照 - :param shared_sections: 页面共享分区列表 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.record = record - self.shared_sections = shared_sections - self.search_query = query - self.rendering = rendering or WORKSPACE_RENDERING - super().__init__(id='record-detail-view', markup=False) - self.can_focus = True - self._render_record() - - def show_record( - self, - record: BrowserRecordSnapshot, - shared_sections: list[DetailSectionSnapshot], - query: str = '', - ) -> None: - """ - 切换并渲染当前展示的记录内容。 - - :param record: 待展示的记录快照 - :param shared_sections: 页面共享分区列表 - :return: None - """ - self.record = record - self.shared_sections = shared_sections - self.search_query = query - self._render_record() - - def _render_record(self) -> None: - """ - 渲染当前记录内容。 - - :return: None - """ - self.remove_class('is-ok', 'is-fail', 'is-warn', 'is-info') - self.add_class(self.rendering.resolve_status_class(self.record.status)) - lines = [ - ( - f'{self.rendering.render_status_badge(self.record.status)} ' - f'{self.rendering.highlight_search_text(self.record.title, self.search_query)}' - ), - f'{TUI_COPY.build_workspace_label("overview")} · {self.rendering.highlight_search_text(self.record.summary, self.search_query)}', - '', - ] - if self.record.metadata_lines: - lines.append(TUI_COPY.build_workspace_title('key_info')) - lines.append('────────────────') - lines.append( - self.rendering.render_structured_lines( - [ - self.rendering.highlight_search_text(line, self.search_query) - for line in self.record.metadata_lines - ], - TUI_COPY.build_workspace_empty_text('key_info'), - ) - ) - lines.append('') - - combined_sections = [*self.record.detail_sections, *self.shared_sections] - if not combined_sections: - lines.append(TUI_COPY.build_workspace_title('detail_content')) - lines.append('========') - lines.append(f'- {TUI_COPY.build_workspace_empty_text("detail")}') - else: - for section in combined_sections: - lines.append('-' * 44) - lines.append( - f'{self.rendering.render_status_badge(section.status)} ' - f'{self.rendering.highlight_search_text(section.title, self.search_query)}' - ) - lines.append( - self.rendering.render_structured_lines( - [self.rendering.highlight_search_text(line, self.search_query) for line in section.lines], - TUI_COPY.build_workspace_empty_text('detail'), - ) - ) - lines.append('') - self.update('\n'.join(lines).strip()) - - -class RecordSummaryView(Static): - """ - 浏览页记录摘要展示组件。 - - :param record: 当前展示的记录快照 - """ - - DEFAULT_CSS = """ - RecordSummaryView { - border: double #38d8ff; - padding: 1 2; - margin-bottom: 1; - background: #081827; - color: #e9fcff; - overflow-x: auto; - overflow-y: auto; - } - - RecordSummaryView.is-ok { - border: double #22c983; - background: #081b17; - } - - RecordSummaryView.is-fail { - border: double #ff6b7a; - background: #241118; - } - - RecordSummaryView.is-warn { - border: double #f2b84b; - background: #261d0f; - } - - RecordSummaryView.is-info { - border: double #38d8ff; - background: #081825; - } - - RecordSummaryView.section-ok { - border-left: heavy #22c983; - } - - RecordSummaryView.section-fail { - border-left: heavy #ff6b7a; - } - - RecordSummaryView.section-warn { - border-left: heavy #f2b84b; - } - - RecordSummaryView.section-info { - border-left: heavy #38d8ff; - } - - RecordSummaryView:focus { - border: heavy #6df7ff; - background: #10364b; - } - """ - - def __init__( - self, - record: BrowserRecordSnapshot, - rendering: WorkspaceRenderingSupport | None = None, - ) -> None: - """ - 初始化浏览页记录摘要展示组件。 - - :param record: 当前展示的记录快照 - :param rendering: 工作台组件渲染支持对象 - :return: None - """ - self.record = record - self.section: DetailSectionSnapshot | None = None - self.action_lines: list[str] = [] - self.search_query = '' - self.rendering = rendering or WORKSPACE_RENDERING - super().__init__(id='record-summary-view', markup=False) - self.can_focus = True - self._render_record() - - def show_record( - self, - record: BrowserRecordSnapshot, - selected_section: DetailSectionSnapshot | None = None, - action_lines: list[str] | None = None, - query: str = '', - ) -> None: - """ - 更新当前展示的记录摘要。 - - :param record: 待展示的记录快照 - :param selected_section: 当前联动分区 - :param action_lines: 动作摘要文本 - :return: None - """ - self.record = record - self.section = selected_section - self.action_lines = list(action_lines or []) - self.search_query = query - self._render_record() - - def _render_record(self) -> None: - """ - 渲染当前记录摘要。 - - :return: None - """ - self.remove_class('is-ok', 'is-fail', 'is-warn', 'is-info') - self.remove_class('section-ok', 'section-fail', 'section-warn', 'section-info') - self.add_class(self.rendering.resolve_status_class(self.record.status)) - if self.section is not None: - self.add_class( - f'section-{self.section.status if self.section.status in {"ok", "fail", "warn"} else "info"}' - ) - lines = [ - ( - f'{self.rendering.render_status_badge(self.record.status)} ' - f'{self.rendering.highlight_search_text(self.record.title, self.search_query)}' - ), - f'{TUI_COPY.build_workspace_label("overview")} · {self.rendering.highlight_search_text(self.record.summary, self.search_query)}', - ] - if self.section is not None: - lines.append( - f'{TUI_COPY.build_workspace_label("current_section")} · {self.rendering.highlight_search_text(self.section.title, self.search_query)} / {TUI_COPY.render_status_label(self.section.status)}' - ) - if self.record.metadata_lines: - lines.append('') - lines.append(TUI_COPY.build_workspace_title('key_fields')) - lines.append('────────────────') - lines.append( - self.rendering.render_structured_lines( - [ - self.rendering.highlight_search_text(line, self.search_query) - for line in self.record.metadata_lines - ], - TUI_COPY.build_workspace_empty_text('key_info'), - ) - ) - if self.action_lines: - lines.append('') - lines.append(TUI_COPY.build_workspace_title('action_feedback')) - lines.append('────────────────') - lines.append( - self.rendering.render_structured_lines( - [self.rendering.highlight_search_text(line, self.search_query) for line in self.action_lines], - TUI_COPY.build_workspace_empty_text('actions'), - ) - ) - self.update('\n'.join(lines)) diff --git a/shierfeng-fastapi-backend/build/lib/cli/utils.py b/shierfeng-fastapi-backend/build/lib/cli/utils.py deleted file mode 100644 index 28e4221..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/utils.py +++ /dev/null @@ -1,366 +0,0 @@ -import json -import os -import re -import shlex -import subprocess -import sys -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -from cli.exit_codes import RUNTIME_ERROR -from cli.runtime.base import RUNTIME_ENVIRONMENT - -_SNAKE_CASE_BOUNDARY_PATTERN = re.compile(r'(? str: - """ - 将命令参数列表格式化为 shell 可读文本。 - - :param arguments: 命令参数列表 - :return: shell 命令文本 - """ - return shlex.join(arguments) - - @staticmethod - def truncate_text(value: object, max_length: int) -> str: - """ - 将文本截断到指定长度。 - - :param value: 原始文本值 - :param max_length: 最大长度 - :return: 截断后的文本 - """ - text = '' if value is None else str(value).strip() - if len(text) <= max_length: - return text - return f'{text[: max_length - 3]}...' - - @staticmethod - def to_snake_case(value: object) -> str: - """ - 将字段名标准化为 `snake_case` 形式。 - - :param value: 原始字段名 - :return: `snake_case` 字段名 - """ - text = '' if value is None else str(value).strip() - if not text: - return '' - normalized_text = text.replace('-', '_').replace(' ', '_') - return _SNAKE_CASE_BOUNDARY_PATTERN.sub('_', normalized_text).lower() - - -@dataclass(frozen=True) -class NestedCliProjectLocator: - """ - 内部 CLI 项目目录定位器。 - - 该对象负责识别后端根目录,并为内部 CLI 调用返回稳定的项目目录。 - """ - - @staticmethod - def is_backend_project_dir(project_dir: Path) -> bool: - """ - 判断给定目录是否为后端项目根目录。 - - :param project_dir: 待检查目录 - :return: 是否为后端项目根目录 - """ - return RUNTIME_ENVIRONMENT.is_backend_project_dir(project_dir) - - def resolve_project_dir(self) -> Path: - """ - 解析内部 CLI 子进程应使用的后端项目目录。 - - 优先使用当前工作目录;若当前目录不是后端目录,则回退到当前 - `cli/utils.py` 所在代码树的上一层目录。 - - :return: 后端项目根目录 - """ - current_dir = Path.cwd().resolve() - if self.is_backend_project_dir(current_dir): - return current_dir - return Path(__file__).resolve().parents[1] - - -@dataclass(frozen=True) -class NestedCliEnvironmentBuilder: - """ - 内部 CLI 环境构建器。 - - 该对象负责构建子进程环境变量以及内部 CLI 调用命令。 - """ - - @staticmethod - def build_process_env(project_dir: Path) -> dict[str, str]: - """ - 构建内部 CLI 子进程环境变量。 - - :param project_dir: 后端项目根目录 - :return: 子进程环境变量字典 - """ - process_env = dict(os.environ) - project_dir_str = str(project_dir) - python_path = process_env.get('PYTHONPATH', '').strip() - if python_path: - process_env['PYTHONPATH'] = os.pathsep.join([project_dir_str, python_path]) - else: - process_env['PYTHONPATH'] = project_dir_str - return process_env - - @staticmethod - def build_nested_cli_command(*arguments: str) -> list[str]: - """ - 构建内部 CLI 调用命令。 - - :param arguments: CLI 参数列表 - :return: 完整子进程命令参数列表 - """ - return [sys.executable, '-m', 'cli.main', '--color=never', '--icon=none', *arguments] - - -@dataclass(frozen=True) -class NestedCliPayloadParser: - """ - 内部 CLI 负载解析器。 - - 该对象负责从标准输出/错误中提取 JSON,并为非 JSON 结果构建兜底负载。 - """ - - @staticmethod - def extract_json_payload(output_text: str) -> dict[str, Any] | list[Any] | None: - """ - 从原始输出文本中提取 JSON 负载。 - - 该逻辑优先尝试直接解析;若输出前后混入提示文本、ANSI 控制序列 - 或其他噪声,则尝试截取最外层 JSON 对象或数组再解析。 - - :param output_text: 原始输出文本 - :return: 解析成功的 JSON 负载,失败时返回 None - """ - normalized_output = _ANSI_ESCAPE_PATTERN.sub('', output_text).strip() - if not normalized_output: - return None - - decoder = json.JSONDecoder() - try: - return decoder.decode(normalized_output) - except Exception: - pass - - for index, char in enumerate(normalized_output): - if char not in '{[': - continue - try: - payload, _ = decoder.raw_decode(normalized_output[index:]) - return payload - except Exception: - continue - return None - - @staticmethod - def build_non_json_payload_fallback( - *, - stdout: str, - stderr: str, - returncode: int, - ) -> dict[str, Any]: - """ - 为未返回 JSON 的内部 CLI 输出构建兜底负载。 - - :param stdout: 标准输出文本 - :param stderr: 标准错误文本 - :param returncode: 进程退出码 - :return: 兜底结果负载 - """ - stdout_text = stdout.strip() - stderr_text = stderr.strip() - summary = stdout_text or stderr_text or '内部 CLI 未返回可解析的 JSON 输出' - return { - 'ok': returncode == 0, - 'message': summary, - 'stdout': stdout_text, - 'stderr': stderr_text, - 'exit_code': returncode or RUNTIME_ERROR, - 'fallback': 'non_json_output', - } - - -@dataclass -class NestedCliSupport: - """ - 内部 CLI 子进程支持服务。 - - 该对象统一负责编排项目目录解析、环境注入、标准输出解析以及 - 实时执行/进程替换执行路径。 - - :param project_locator: 项目目录定位器 - :param environment_builder: 子进程环境构建器 - :param payload_parser: 负载解析器 - """ - - project_locator: NestedCliProjectLocator = field(default_factory=NestedCliProjectLocator) - environment_builder: NestedCliEnvironmentBuilder = field(default_factory=NestedCliEnvironmentBuilder) - payload_parser: NestedCliPayloadParser = field(default_factory=NestedCliPayloadParser) - - def resolve_project_dir(self) -> Path: - """ - 解析内部 CLI 子进程应使用的后端项目目录。 - - :return: 后端项目根目录 - """ - return self.project_locator.resolve_project_dir() - - def build_process_env(self, project_dir: Path) -> dict[str, str]: - """ - 构建内部 CLI 子进程环境变量。 - - :param project_dir: 后端项目根目录 - :return: 子进程环境变量字典 - """ - return self.environment_builder.build_process_env(project_dir) - - def build_nested_cli_command(self, *arguments: str) -> list[str]: - """ - 构建内部 CLI 调用命令。 - - :param arguments: CLI 参数列表 - :return: 完整子进程命令参数列表 - """ - return self.environment_builder.build_nested_cli_command(*arguments) - - def extract_json_payload(self, output_text: str) -> dict[str, Any] | list[Any] | None: - """ - 从原始输出文本中提取 JSON 负载。 - - :param output_text: 原始输出文本 - :return: 解析成功的 JSON 负载,失败时返回 None - """ - return self.payload_parser.extract_json_payload(output_text) - - def build_non_json_payload_fallback( - self, - *, - stdout: str, - stderr: str, - returncode: int, - ) -> dict[str, Any]: - """ - 为未返回 JSON 的内部 CLI 输出构建兜底负载。 - - :param stdout: 标准输出文本 - :param stderr: 标准错误文本 - :param returncode: 进程退出码 - :return: 兜底结果负载 - """ - return self.payload_parser.build_non_json_payload_fallback( - stdout=stdout, - stderr=stderr, - returncode=returncode, - ) - - def run(self, *arguments: str, parse_json: bool = False) -> NestedCliResult: - """ - 在当前后端目录中执行内部 CLI 命令。 - - :param arguments: CLI 参数列表 - :param parse_json: 是否尝试解析标准输出中的 JSON 负载 - :return: 内部 CLI 调用结果 - """ - project_dir = self.resolve_project_dir() - command = self.build_nested_cli_command(*arguments) - completed = subprocess.run( - command, - capture_output=True, - text=True, - check=False, - cwd=str(project_dir), - env=self.build_process_env(project_dir), - ) - payload: dict[str, Any] | list[Any] | None = None - if parse_json: - payload = self.extract_json_payload(completed.stdout) - if payload is None: - payload = self.extract_json_payload(completed.stderr) - if payload is None: - payload = self.build_non_json_payload_fallback( - stdout=completed.stdout, - stderr=completed.stderr, - returncode=completed.returncode, - ) - return NestedCliResult( - command=command, - returncode=completed.returncode, - stdout=completed.stdout, - stderr=completed.stderr, - payload=payload, - ) - - def run_live(self, *arguments: str) -> subprocess.CompletedProcess[str]: - """ - 在当前终端中直接执行内部 CLI 命令。 - - 该模式不会捕获标准输出/错误,适用于需要占用当前终端交互的 wizard - 或其他外部命令入口;调用方应自行决定是否先挂起 TUI。 - - :param arguments: CLI 参数列表 - :return: 子进程执行结果 - """ - project_dir = self.resolve_project_dir() - command = self.build_nested_cli_command(*arguments) - return subprocess.run( - command, - text=True, - check=False, - cwd=str(project_dir), - env=self.build_process_env(project_dir), - ) - - def exec(self, *arguments: str) -> None: - """ - 以进程替换方式执行内部 CLI 命令。 - - :param arguments: CLI 参数列表 - :return: None - """ - project_dir = self.resolve_project_dir() - command = self.build_nested_cli_command(*arguments) - executable = command[0] - subprocess_arguments = [executable, *command[1:]] - os.chdir(project_dir) - os.execvp(executable, subprocess_arguments) - - -SHELL_TEXT_FORMATTER = ShellTextFormatter() -NESTED_CLI_SUPPORT = NestedCliSupport() diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/aggregators.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/aggregators.py deleted file mode 100644 index ade4639..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/aggregators.py +++ /dev/null @@ -1,122 +0,0 @@ -from dataclasses import dataclass -from typing import Any - - -@dataclass(frozen=True) -class ProdCheckPayloadSupport: - """ - `wizard prod-check` 结果提取支持对象。 - - 该对象负责从多路只读结果中提取 runtime、doctor、config - 片段,避免聚合器本体继续堆叠细碎字段判定逻辑。 - """ - - @staticmethod - def extract_runtime_payload(runtime_payload: dict[str, Any] | None) -> dict[str, Any] | None: - """ - 提取运行环境结果中的 runtime 片段。 - - :param runtime_payload: 运行环境结果 - :return: runtime 片段 - """ - return runtime_payload.get('runtime') if isinstance(runtime_payload, dict) else None - - @staticmethod - def extract_doctor_payload(doctor_payload: dict[str, Any] | None) -> dict[str, Any] | None: - """ - 提取应用检查结果片段。 - - :param doctor_payload: 应用检查结果 - :return: doctor 片段 - """ - return doctor_payload if isinstance(doctor_payload, dict) else None - - @staticmethod - def extract_config_payload(config_payload: dict[str, Any] | None) -> dict[str, Any] | None: - """ - 提取配置快照结果中的 config 片段。 - - :param config_payload: 配置快照结果 - :return: config 片段 - """ - return config_payload.get('config') if isinstance(config_payload, dict) else None - - -@dataclass(frozen=True) -class ProdCheckStatusEvaluator: - """ - `wizard prod-check` 状态评估器。 - - 该对象负责统一计算聚合结果的成功状态和标准消息。 - """ - - @staticmethod - def is_ok( - runtime_payload: dict[str, Any] | None, - doctor_payload: dict[str, Any] | None, - config_payload: dict[str, Any] | None, - ) -> bool: - """ - 计算生产巡检聚合结果是否成功。 - - :param runtime_payload: 运行环境结果 - :param doctor_payload: 应用检查结果 - :param config_payload: 配置快照结果 - :return: 是否成功 - """ - return ( - bool(runtime_payload and runtime_payload.get('ok', False)) - and bool(doctor_payload and doctor_payload.get('ok', False)) - and (config_payload is None or bool(config_payload.get('ok', False))) - ) - - @staticmethod - def build_message() -> str: - """ - 构建生产巡检统一结果消息。 - - :return: 结果消息 - """ - return '生产巡检完成' - - -@dataclass(frozen=True) -class ProdCheckAggregator: - """ - `wizard prod-check` 聚合器。 - - 该对象负责聚合运行环境、应用检查和配置快照结果, - 让 flow 本体只保留交互编排职责。 - - :param payload_support: 结果提取支持对象 - :param status_evaluator: 状态评估器 - """ - - payload_support: ProdCheckPayloadSupport = ProdCheckPayloadSupport() - status_evaluator: ProdCheckStatusEvaluator = ProdCheckStatusEvaluator() - - def build_payload( - self, - *, - env: str, - runtime_payload: dict[str, Any] | None, - doctor_payload: dict[str, Any] | None, - config_payload: dict[str, Any] | None, - ) -> dict[str, Any]: - """ - 构建生产巡检聚合结果。 - - :param env: 当前运行环境 - :param runtime_payload: 运行环境结果 - :param doctor_payload: 应用检查结果 - :param config_payload: 配置快照结果 - :return: 聚合结果负载 - """ - return { - 'ok': self.status_evaluator.is_ok(runtime_payload, doctor_payload, config_payload), - 'env': env, - 'message': self.status_evaluator.build_message(), - 'runtime': self.payload_support.extract_runtime_payload(runtime_payload), - 'doctor': self.payload_support.extract_doctor_payload(doctor_payload), - 'config': self.payload_support.extract_config_payload(config_payload), - } diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/base.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/base.py deleted file mode 100644 index b699087..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/base.py +++ /dev/null @@ -1,909 +0,0 @@ -import inspect -from abc import ABC, abstractmethod -from collections.abc import Callable -from typing import Any, ClassVar, Generic, TypeVar - -import typer - -from cli.context import CliContext, OutputOption -from cli.core import DEFAULT_CORE_SERVICES, CliContextFactory, CliExecutionService -from cli.output import CommandResult -from cli.utils import NESTED_CLI_SUPPORT -from cli.wizard.preview import WIZARD_PREVIEW_RENDERER, WizardPreviewRenderer -from cli.wizard.prompts import WIZARD_PROMPT_SERVICE, WizardPromptService - -SelectionT = TypeVar('SelectionT') - - -class WizardInteractionSupport: - """ - 向导交互与上下文构建支持对象。 - - :param context_factory: CLI 上下文工厂 - :param prompt_service: 向导提示服务 - """ - - def __init__( - self, - *, - context_factory: CliContextFactory, - prompt_service: WizardPromptService, - ) -> None: - """ - 初始化交互支持对象。 - - :param context_factory: CLI 上下文工厂 - :param prompt_service: 向导提示服务 - :return: None - """ - self.context_factory = context_factory - self.prompt_service = prompt_service - - def build_regular_context( - self, - env: str, - output: OutputOption, - *, - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> CliContext: - """ - 构建普通可写命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境执行 - :param yes: 是否跳过确认 - :param dry_run: 是否执行预演 - :return: CLI 上下文 - """ - return self.context_factory.build_regular(env, output, allow_prod, yes, dry_run) - - def build_readonly_context(self, env: str, output: OutputOption) -> CliContext: - """ - 构建只读命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.context_factory.build_readonly(env, output) - - def prompt_confirm(self, prompt_text: str, *, default_value: bool) -> bool: - """ - 执行标准确认交互。 - - :param prompt_text: 提示文本 - :param default_value: 默认值 - :return: 是否确认 - """ - return self.prompt_service.prompt_confirm(prompt_text, default_value=default_value) - - -class WizardResultSupport: - """ - 向导结果收口支持对象。 - - :param execution_service: CLI 执行服务 - :param preview_renderer: 向导预览渲染器 - """ - - def __init__( - self, - *, - execution_service: CliExecutionService, - preview_renderer: WizardPreviewRenderer, - ) -> None: - """ - 初始化结果收口支持对象。 - - :param execution_service: CLI 执行服务 - :param preview_renderer: 向导预览渲染器 - :return: None - """ - self.execution_service = execution_service - self.preview_renderer = preview_renderer - - def complete_result(self, ctx: CliContext, result: CommandResult) -> None: - """ - 输出命令结果对象。 - - :param ctx: CLI 上下文 - :param result: 命令结果对象 - :return: None - """ - self.execution_service.complete_result(ctx, result) - - def complete_payload( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - default_exit_code: int = 0, - ) -> None: - """ - 输出标准结果负载。 - - :param ctx: CLI 上下文 - :param payload: 标准结果负载 - :param default_exit_code: 默认退出码 - :return: None - """ - self.execution_service.complete_payload(ctx, payload, default_exit_code=default_exit_code) - - def complete_payload_result( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - text_builder: Callable[[dict[str, Any]], Any] | None = None, - default_exit_code: int = 0, - text_condition: Callable[[dict[str, Any]], bool] | None = None, - ) -> None: - """ - 统一按输出格式收口标准结果负载。 - - :param ctx: CLI 上下文 - :param payload: 标准结果负载 - :param text_builder: 文本结果构建函数 - :param default_exit_code: 默认退出码 - :param text_condition: 文本模式结果构建判定函数 - :return: None - """ - self.execution_service.complete_payload_result( - ctx, - payload, - text_builder=text_builder, - default_exit_code=default_exit_code, - text_condition=text_condition, - ) - - def build_cancel_result(self, wizard_name: str) -> CommandResult: - """ - 构建统一的向导取消结果。 - - :param wizard_name: 向导名称 - :return: 命令结果对象 - """ - return self.preview_renderer.build_cancel_result(wizard_name) - - def get_preview_renderer(self) -> WizardPreviewRenderer: - """ - 返回当前项目默认的向导预览渲染器。 - - :return: 预览渲染器 - """ - return self.preview_renderer - - -class WizardNestedCommandSupport: - """ - 向导内部 CLI 调用支持对象。 - - :param command_runner: 内部 CLI 子进程调用函数 - :param live_command_runner: 内部 CLI 实时执行函数 - """ - - def __init__( - self, - *, - command_runner: Callable[..., Any], - live_command_runner: Callable[..., Any], - ) -> None: - """ - 初始化内部 CLI 调用支持对象。 - - :param command_runner: 内部 CLI 子进程调用函数 - :param live_command_runner: 内部 CLI 实时执行函数 - :return: None - """ - self.command_runner = command_runner - self.live_command_runner = live_command_runner - - @staticmethod - def resolve_callable_override(owner: type['BaseCliWizardFlow[Any]'], attribute_name: str) -> Callable[..., Any]: - """ - 解析类级可调用注入点,避免普通函数在实例访问时被绑定为方法。 - - :param owner: 向导类 - :param attribute_name: 类属性名称 - :return: 可直接调用的函数对象 - """ - attribute = inspect.getattr_static(owner, attribute_name) - if isinstance(attribute, staticmethod): - return attribute.__func__ - return attribute - - def run_nested_command(self, *arguments: str, parse_json: bool = False) -> Any: - """ - 调用内部 CLI 并按需解析 JSON。 - - :param arguments: CLI 参数列表 - :param parse_json: 是否解析 JSON - :return: 内部 CLI 调用结果 - """ - return self.command_runner(*arguments, parse_json=parse_json) - - def exec_nested_command(self, *arguments: str) -> Any: - """ - 直接切换到内部 CLI 的实时执行路径。 - - :param arguments: CLI 参数列表 - :return: 内部 CLI 调用结果 - """ - return self.live_command_runner(*arguments) - - -class WizardExecutionResultSupport: - """ - 向导执行结果翻译支持对象。 - - 该对象负责提取内部 CLI 结果的 payload、退出码与错误文本,并构建 - 默认失败负载,让模板基类不再直接持有结果翻译细节。 - - :param failure_message: 默认失败提示 - """ - - def __init__(self, *, failure_message: str) -> None: - """ - 初始化执行结果翻译支持对象。 - - :param failure_message: 默认失败提示 - :return: None - """ - self.failure_message = failure_message - - @staticmethod - def extract_payload(nested_result: Any) -> dict[str, Any] | None: - """ - 提取内部 CLI JSON 负载。 - - :param nested_result: 内部 CLI 调用结果 - :return: JSON 负载 - """ - payload = getattr(nested_result, 'payload', None) - return payload if isinstance(payload, dict) else None - - @staticmethod - def extract_returncode(nested_result: Any) -> int: - """ - 提取内部 CLI 退出码。 - - :param nested_result: 内部 CLI 调用结果 - :return: 退出码 - """ - return int(getattr(nested_result, 'returncode', 0) or 0) - - @staticmethod - def extract_stdout(nested_result: Any) -> str: - """ - 提取内部 CLI 标准输出。 - - :param nested_result: 内部 CLI 调用结果 - :return: 标准输出文本 - """ - return str(getattr(nested_result, 'stdout', '') or '').strip() - - @staticmethod - def extract_stderr(nested_result: Any) -> str: - """ - 提取内部 CLI 标准错误。 - - :param nested_result: 内部 CLI 调用结果 - :return: 标准错误文本 - """ - return str(getattr(nested_result, 'stderr', '') or '').strip() - - def build_failure_payload(self, nested_result: Any) -> dict[str, Any]: - """ - 为内部 CLI 非 JSON 结果构建失败负载。 - - :param nested_result: 内部 CLI 调用结果 - :return: 失败负载 - """ - error_text = self.extract_stderr(nested_result) or self.extract_stdout(nested_result) - return { - 'ok': False, - 'message': self.failure_message, - 'error': error_text, - 'exit_code': self.extract_returncode(nested_result), - } - - -class WizardSupportCache: - """ - 向导协作者缓存。 - - 该对象负责延迟构建并复用当前 flow 需要的 interaction/result/nested - support collaborator,让 `BaseCliWizardFlow` 回到模板骨架与桥接层角色。 - - :param owner: 当前向导 flow - """ - - def __init__(self, owner: 'BaseCliWizardFlow[Any]') -> None: - """ - 初始化向导协作者缓存。 - - :param owner: 当前向导 flow - :return: None - """ - self.owner = owner - self._interaction_support: WizardInteractionSupport | None = None - self._result_support: WizardResultSupport | None = None - self._nested_command_support: WizardNestedCommandSupport | None = None - self._execution_result_support: WizardExecutionResultSupport | None = None - - def get_interaction_support(self) -> WizardInteractionSupport: - """ - 返回当前向导使用的交互支持对象。 - - :return: 交互支持对象 - """ - if self._interaction_support is None: - self._interaction_support = WizardInteractionSupport( - context_factory=self.owner.context_factory, - prompt_service=self.owner.prompt_service, - ) - return self._interaction_support - - def get_result_support(self) -> WizardResultSupport: - """ - 返回当前向导使用的结果收口支持对象。 - - :return: 结果收口支持对象 - """ - if self._result_support is None: - self._result_support = WizardResultSupport( - execution_service=self.owner.execution_service, - preview_renderer=self.owner.preview_renderer, - ) - return self._result_support - - def get_nested_command_support(self) -> WizardNestedCommandSupport: - """ - 返回当前向导使用的内部 CLI 调用支持对象。 - - :return: 内部 CLI 调用支持对象 - """ - if self._nested_command_support is None: - self._nested_command_support = WizardNestedCommandSupport( - command_runner=WizardNestedCommandSupport.resolve_callable_override( - type(self.owner), 'nested_command_runner' - ), - live_command_runner=WizardNestedCommandSupport.resolve_callable_override( - type(self.owner), - 'nested_live_command_runner', - ), - ) - return self._nested_command_support - - def get_execution_result_support(self) -> WizardExecutionResultSupport: - """ - 返回当前向导使用的执行结果翻译支持对象。 - - :return: 执行结果翻译支持对象 - """ - if self._execution_result_support is None: - self._execution_result_support = WizardExecutionResultSupport( - failure_message=self.owner.failure_message, - ) - return self._execution_result_support - - -class BaseNestedCommandWizardFlow(ABC, Generic[SelectionT]): - """ - 统一封装通过内部 CLI 执行的向导流程。 - - 子类需要提供参数采集、上下文构建、预览摘要、执行参数和 - 结果收口策略,从而复用向导的通用交互主链路。 - """ - - wizard_name: str - preview_title: str - failure_message: str - - def run(self, output: OutputOption = 'text') -> None: - """ - 执行向导主流程。 - - :param output: 输出格式 - :return: None - """ - selection = self.collect_selection() - self.prepare_execution_state(selection, output) - ctx = self.prepare_context(selection, output) - typer.echo(self.render_preview(selection)) - if not self.prompt_confirm(self.confirm_prompt(selection), default_value=self.confirm_default_value(selection)): - self.complete_result(ctx, self.build_cancel_result()) - return - - nested_result = self.execute(selection, output) - if self.handle_execution_result(ctx, nested_result, output): - return - payload = self.extract_payload(nested_result) - if payload is not None: - self.complete_payload(ctx, payload, default_exit_code=self.extract_returncode(nested_result)) - return - self.complete_payload(ctx, self.build_failure_payload(nested_result)) - - def render_preview(self, selection: SelectionT) -> str: - """ - 生成向导预览文本。 - - :param selection: 向导采集结果 - :return: 预览文本 - """ - return self.get_preview_renderer().render_preview( - self.preview_title, - summary=self.build_preview_summary(selection), - command=self.build_preview_command(selection), - notes=self.build_preview_notes(selection), - ) - - def confirm_default_value(self, selection: SelectionT) -> bool: - """ - 返回最终确认默认值。 - - :param selection: 向导采集结果 - :return: 默认确认值 - """ - del selection - return False - - def build_preview_notes(self, selection: SelectionT) -> list[str] | None: - """ - 构建预览附加说明。 - - :param selection: 向导采集结果 - :return: 预览附加说明 - """ - del selection - return None - - def prepare_execution_state(self, selection: SelectionT, output: OutputOption) -> None: - """ - 在渲染预览前准备 flow 级执行状态。 - - 该钩子用于少量需要在预览、确认默认值或执行前共享状态的 flow, - 例如预先探测只读诊断结果,但不允许子类因此重写整个模板主链路。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: None - """ - del selection, output - - def handle_execution_result(self, ctx: CliContext, nested_result: Any, output: OutputOption) -> bool: - """ - 允许子类在默认 payload 收口前接管执行结果。 - - 该钩子适用于需要切换到 live exec、实时终端占用或其他非标准 payload - 收口路径的少数 flow。返回 `True` 表示结果已处理完成。 - - :param ctx: CLI 上下文 - :param nested_result: 执行结果 - :param output: 输出格式 - :return: 是否已完成结果处理 - """ - del ctx, nested_result, output - return False - - def build_failure_payload(self, nested_result: Any) -> dict[str, Any]: - """ - 为内部 CLI 非 JSON 结果构建失败负载。 - - :param nested_result: 内部 CLI 调用结果 - :return: 失败负载 - """ - return self.get_execution_result_support().build_failure_payload(nested_result) - - @staticmethod - def extract_payload(nested_result: Any) -> dict[str, Any] | None: - """ - 提取内部 CLI JSON 负载。 - - :param nested_result: 内部 CLI 调用结果 - :return: JSON 负载 - """ - return WizardExecutionResultSupport.extract_payload(nested_result) - - @staticmethod - def extract_returncode(nested_result: Any) -> int: - """ - 提取内部 CLI 退出码。 - - :param nested_result: 内部 CLI 调用结果 - :return: 退出码 - """ - return WizardExecutionResultSupport.extract_returncode(nested_result) - - @staticmethod - def extract_stdout(nested_result: Any) -> str: - """ - 提取内部 CLI 标准输出。 - - :param nested_result: 内部 CLI 调用结果 - :return: 标准输出文本 - """ - return WizardExecutionResultSupport.extract_stdout(nested_result) - - @staticmethod - def extract_stderr(nested_result: Any) -> str: - """ - 提取内部 CLI 标准错误。 - - :param nested_result: 内部 CLI 调用结果 - :return: 标准错误文本 - """ - return WizardExecutionResultSupport.extract_stderr(nested_result) - - @abstractmethod - def collect_selection(self) -> SelectionT: - """ - 采集向导参数。 - - :return: 向导采集结果 - """ - - @abstractmethod - def prepare_context(self, selection: SelectionT, output: OutputOption) -> CliContext: - """ - 构建命令上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - - @abstractmethod - def build_preview_summary(self, selection: SelectionT) -> dict[str, Any]: - """ - 构建预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - - @abstractmethod - def build_preview_command(self, selection: SelectionT) -> list[str]: - """ - 构建用户视角命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - - @abstractmethod - def confirm_prompt(self, selection: SelectionT) -> str: - """ - 返回最终确认提示文本。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - - @abstractmethod - def build_execute_arguments(self, selection: SelectionT, output: OutputOption) -> list[str]: - """ - 构建内部 CLI 执行参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - - @abstractmethod - def prompt_confirm(self, prompt_text: str, *, default_value: bool) -> bool: - """ - 执行确认交互。 - - :param prompt_text: 提示文本 - :param default_value: 默认值 - :return: 是否确认 - """ - - @abstractmethod - def complete_result(self, ctx: CliContext, result: CommandResult) -> None: - """ - 输出命令结果对象。 - - :param ctx: CLI 上下文 - :param result: 命令结果 - :return: None - """ - - @abstractmethod - def build_cancel_result(self) -> CommandResult: - """ - 构建向导取消结果。 - - :return: 命令结果对象 - """ - - @abstractmethod - def get_preview_renderer(self) -> WizardPreviewRenderer: - """ - 获取当前向导使用的预览渲染器。 - - :return: 预览渲染器 - """ - - @abstractmethod - def complete_payload( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - default_exit_code: int = 0, - ) -> None: - """ - 输出标准负载。 - - :param ctx: CLI 上下文 - :param payload: 结果负载 - :param default_exit_code: 默认退出码 - :return: None - """ - - def execute(self, selection: SelectionT, output: OutputOption) -> Any: - """ - 执行内部 CLI 命令。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 调用结果 - """ - return self.run_nested_command(*self.build_execute_arguments(selection, output), parse_json=True) - - @abstractmethod - def run_nested_command(self, *arguments: str, parse_json: bool = False) -> Any: - """ - 调用内部 CLI。 - - :param arguments: 参数列表 - :param parse_json: 是否解析 JSON - :return: 内部 CLI 调用结果 - """ - - -class BaseCliWizardFlow(BaseNestedCommandWizardFlow[SelectionT], ABC): - """ - 当前项目使用的 CLI 向导基类。 - - 该基类在通用向导主链路之上继续下沉当前项目稳定复用的桥接逻辑, - 包括确认交互、上下文构建、命令结果收口与内部 CLI 调用封装。 - """ - - context_factory: ClassVar[CliContextFactory] = DEFAULT_CORE_SERVICES.context_factory - execution_service: ClassVar[CliExecutionService] = DEFAULT_CORE_SERVICES.execution_service - prompt_service: ClassVar[WizardPromptService] = WIZARD_PROMPT_SERVICE - preview_renderer: ClassVar[WizardPreviewRenderer] = WIZARD_PREVIEW_RENDERER - nested_command_runner: ClassVar[Callable[..., Any]] = staticmethod(NESTED_CLI_SUPPORT.run) - nested_live_command_runner: ClassVar[Callable[..., Any]] = staticmethod(NESTED_CLI_SUPPORT.exec) - - def __init__(self) -> None: - """ - 初始化当前项目向导基类。 - - 该基类通过协作者缓存稳定持有交互、结果收口和内部 CLI 调用 support, - 避免模板方法执行过程中重复构建同类对象。 - - :return: None - """ - self._support_cache: WizardSupportCache | None = None - - def get_support_cache(self) -> WizardSupportCache: - """ - 返回当前向导使用的协作者缓存。 - - 保持惰性创建,以兼容未显式调用基类 `__init__` 的现有 flow。 - - :return: 协作者缓存 - """ - support_cache = getattr(self, '_support_cache', None) - if support_cache is None: - self._support_cache = WizardSupportCache(self) - return self._support_cache - - def get_interaction_support(self) -> WizardInteractionSupport: - """ - 返回当前向导使用的交互支持对象。 - - :return: 交互支持对象 - """ - return self.get_support_cache().get_interaction_support() - - def get_result_support(self) -> WizardResultSupport: - """ - 返回当前向导使用的结果收口支持对象。 - - :return: 结果收口支持对象 - """ - return self.get_support_cache().get_result_support() - - def get_nested_command_support(self) -> WizardNestedCommandSupport: - """ - 返回当前向导使用的内部 CLI 调用支持对象。 - - :return: 内部 CLI 调用支持对象 - """ - return self.get_support_cache().get_nested_command_support() - - def get_execution_result_support(self) -> WizardExecutionResultSupport: - """ - 返回当前向导使用的执行结果翻译支持对象。 - - :return: 执行结果翻译支持对象 - """ - return self.get_support_cache().get_execution_result_support() - - def build_regular_context( - self, - env: str, - output: OutputOption, - *, - allow_prod: bool, - yes: bool, - dry_run: bool, - ) -> CliContext: - """ - 构建普通可写命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :param allow_prod: 是否允许生产环境执行 - :param yes: 是否跳过确认 - :param dry_run: 是否执行预演 - :return: CLI 上下文 - """ - return self.get_interaction_support().build_regular_context( - env, - output, - allow_prod=allow_prod, - yes=yes, - dry_run=dry_run, - ) - - def build_readonly_context(self, env: str, output: OutputOption) -> CliContext: - """ - 构建只读命令上下文。 - - :param env: 运行环境 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.get_interaction_support().build_readonly_context(env, output) - - def prompt_confirm(self, prompt_text: str, *, default_value: bool) -> bool: - """ - 执行标准确认交互。 - - :param prompt_text: 提示文本 - :param default_value: 默认值 - :return: 是否确认 - """ - return self.get_interaction_support().prompt_confirm(prompt_text, default_value=default_value) - - def complete_result(self, ctx: CliContext, result: CommandResult) -> None: - """ - 输出命令结果对象。 - - :param ctx: CLI 上下文 - :param result: 命令结果对象 - :return: None - """ - self.get_result_support().complete_result(ctx, result) - - def complete_payload( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - default_exit_code: int = 0, - ) -> None: - """ - 输出标准结果负载。 - - :param ctx: CLI 上下文 - :param payload: 标准结果负载 - :param default_exit_code: 默认退出码 - :return: None - """ - self.get_result_support().complete_payload(ctx, payload, default_exit_code=default_exit_code) - - def complete_payload_result( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - text_builder: Callable[[dict[str, Any]], Any] | None = None, - default_exit_code: int = 0, - text_condition: Callable[[dict[str, Any]], bool] | None = None, - ) -> None: - """ - 统一按输出格式收口标准结果负载。 - - :param ctx: CLI 上下文 - :param payload: 标准结果负载 - :param text_builder: 文本结果构建函数 - :param default_exit_code: 默认退出码 - :param text_condition: 文本模式结果构建判定函数 - :return: None - """ - self.get_result_support().complete_payload_result( - ctx, - payload, - text_builder=text_builder, - default_exit_code=default_exit_code, - text_condition=text_condition, - ) - - def build_cancel_result(self) -> CommandResult: - """ - 构建统一的向导取消结果。 - - :return: 命令结果对象 - """ - return self.get_result_support().build_cancel_result(self.wizard_name) - - def get_preview_renderer(self) -> WizardPreviewRenderer: - """ - 返回当前项目默认的向导预览渲染器。 - - :return: 预览渲染器 - """ - return self.get_result_support().get_preview_renderer() - - def run_nested_command(self, *arguments: str, parse_json: bool = False) -> Any: - """ - 调用内部 CLI 并按需解析 JSON。 - - :param arguments: CLI 参数列表 - :param parse_json: 是否解析 JSON - :return: 内部 CLI 调用结果 - """ - return self.get_nested_command_support().run_nested_command(*arguments, parse_json=parse_json) - - def exec_nested_command(self, *arguments: str) -> Any: - """ - 直接切换到内部 CLI 的实时执行路径。 - - :param arguments: CLI 参数列表 - :return: 内部 CLI 调用结果 - """ - return self.get_nested_command_support().exec_nested_command(*arguments) - - -class BaseLiveExecCliWizardFlow(BaseCliWizardFlow[SelectionT], ABC): - """ - 统一封装“确认后切换到实时终端执行路径”的 CLI 向导基类。 - - 该特化基类用于少量不走 JSON payload 收口,而是在确认后直接 - 切换到现有命令实时执行链路的向导场景,例如 `wizard app-run`。 - 它在保留预览与确认模板骨架的前提下,显式表达“执行阶段即完成收口”, - 避免子类再通过覆写 `complete_payload()` 或 `handle_execution_result()` - 实现规避式短路。 - """ - - def execute(self, selection: SelectionT, output: OutputOption) -> None: - """ - 切换到内部 CLI 的实时执行路径。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: None - """ - self.exec_nested_command(*self.build_execute_arguments(selection, output)) - - def handle_execution_result(self, ctx: CliContext, nested_result: Any, output: OutputOption) -> bool: - """ - 标记实时执行路径已完成结果收口。 - - :param ctx: CLI 上下文 - :param nested_result: 执行结果 - :param output: 输出格式 - :return: 是否已完成结果处理 - """ - del ctx, nested_result, output - return True diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/commands.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/commands.py deleted file mode 100644 index 18067ac..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/commands.py +++ /dev/null @@ -1,316 +0,0 @@ -from collections.abc import Callable -from dataclasses import dataclass, field -from importlib import import_module - -import typer - -from cli.context import OutputOption - -WizardCommandRegistrar = Callable[[typer.Typer, 'WizardCommandRegistration'], None] - - -@dataclass(frozen=True) -class WizardCommandRegistration: - """ - wizard 子命令注册元数据。 - - :param name: 子命令名称 - :param help_text: 子命令帮助文案 - :param registrar: 对应的注册器函数 - """ - - name: str - help_text: str - - -class WizardFlowLoader: - """ - wizard flow 惰性加载器。 - - 该对象负责在命令真正执行时导入对应 flow, - 避免 `wizard` 入口在模块导入阶段提前拉起全部向导依赖。 - """ - - def load_runner(self, module_name: str, function_name: str) -> Callable[..., None]: - """ - 按模块名和函数名加载向导执行入口。 - - :param module_name: flow 模块名 - :param function_name: flow 执行函数名 - :return: 可调用的向导执行函数 - """ - flow_module = import_module(module_name) - return getattr(flow_module, function_name) - - -@dataclass -class WizardCommandBuilder: - """ - wizard 子命令构建器。 - - :param flow_loader: wizard flow 惰性加载器 - """ - - flow_loader: WizardFlowLoader = field(default_factory=WizardFlowLoader) - - def build(self) -> typer.Typer: - """ - 构建 wizard 命令组。 - - :return: wizard 子应用 - """ - app = typer.Typer( - help='交互式向导命令', - no_args_is_help=True, - context_settings={'help_option_names': ['-h', '--help']}, - ) - self.register_commands(app) - return app - - def register_commands(self, app: typer.Typer) -> None: - """ - 向 Typer 子应用注册全部 wizard 子命令。 - - :param app: wizard 子应用 - :return: None - """ - for registration, registrar in self.iter_command_registrars(): - registrar(app, registration) - - def iter_command_registrars(self) -> tuple[tuple[WizardCommandRegistration, WizardCommandRegistrar], ...]: - """ - 返回向导命令注册元数据与显式注册器映射。 - - 该方法将命令声明与注册函数绑定关系显式化,避免继续依赖 - `registrar_name -> getattr(self, ...)` 的字符串分发。 - - :return: 注册元数据与注册器映射列表 - """ - return ( - ( - WizardCommandRegistration('app-run', '通过交互方式启动应用'), - self._register_app_run_command, - ), - ( - WizardCommandRegistration('db-upgrade', '通过交互方式执行数据库升级'), - self._register_db_upgrade_command, - ), - ( - WizardCommandRegistration('cache-clear', '通过交互方式执行缓存清理'), - self._register_cache_clear_command, - ), - ( - WizardCommandRegistration('gen-export', '通过交互方式执行代码导出'), - self._register_gen_export_command, - ), - ( - WizardCommandRegistration('gen-import', '通过交互方式执行物理表导入'), - self._register_gen_import_command, - ), - ( - WizardCommandRegistration('prod-check', '通过交互方式执行生产巡检'), - self._register_prod_check_command, - ), - ) - - def _register_app_run_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `app-run` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def app_run() -> None: - """ - 通过交互方式启动应用。 - - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.app_run', 'run_app_run_wizard')() - - def _register_db_upgrade_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `db-upgrade` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def db_upgrade( - output: OutputOption = 'text', - default_env: str = typer.Option('dev', '--default-env', help='向导默认环境'), - default_revision: str = typer.Option('head', '--default-revision', help='向导默认目标 revision'), - default_dry_run: bool = typer.Option( - True, '--default-dry-run/--no-default-dry-run', help='向导默认 dry-run 选项' - ), - ) -> None: - """ - 通过交互方式执行数据库升级。 - - :param output: 输出格式 - :param default_env: 向导默认环境 - :param default_revision: 向导默认目标 revision - :param default_dry_run: 向导默认 dry-run 选项 - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.db_upgrade', 'run_db_upgrade_wizard')( - output, - default_env=default_env, - default_revision=default_revision, - default_dry_run=default_dry_run, - ) - - def _register_cache_clear_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `cache-clear` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def cache_clear( - output: OutputOption = 'text', - default_env: str = typer.Option('dev', '--default-env', help='向导默认环境'), - default_mode: str = typer.Option('cache-name', '--default-mode', help='向导默认清理模式'), - default_cache_name: str = typer.Option('', '--default-cache-name', help='向导默认缓存名称前缀'), - default_cache_key: str = typer.Option('', '--default-cache-key', help='向导默认缓存键关键字'), - default_dry_run: bool = typer.Option( - True, '--default-dry-run/--no-default-dry-run', help='向导默认 dry-run 选项' - ), - ) -> None: - """ - 通过交互方式执行缓存清理。 - - :param output: 输出格式 - :param default_env: 向导默认环境 - :param default_mode: 向导默认清理模式 - :param default_cache_name: 向导默认缓存名称前缀 - :param default_cache_key: 向导默认缓存键关键字 - :param default_dry_run: 向导默认 dry-run 选项 - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.cache_clear', 'run_cache_clear_wizard')( - output, - default_env=default_env, - default_mode=default_mode, - default_cache_name=default_cache_name, - default_cache_key=default_cache_key, - default_dry_run=default_dry_run, - ) - - def _register_gen_export_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `gen-export` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def gen_export( - output: OutputOption = 'text', - default_env: str = typer.Option('dev', '--default-env', help='向导默认环境'), - default_table_names: str = typer.Option('', '--default-table-names', help='向导默认业务表名称列表'), - default_mode: str = typer.Option('zip', '--default-mode', help='向导默认导出模式'), - default_output_file: str = typer.Option('', '--default-output-file', help='向导默认导出目标文件路径'), - default_dry_run: bool = typer.Option( - True, '--default-dry-run/--no-default-dry-run', help='向导默认 dry-run 选项' - ), - ) -> None: - """ - 通过交互方式执行代码导出。 - - :param output: 输出格式 - :param default_env: 向导默认环境 - :param default_table_names: 向导默认业务表名称列表 - :param default_mode: 向导默认导出模式 - :param default_output_file: 向导默认导出目标文件路径 - :param default_dry_run: 向导默认 dry-run 选项 - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.gen_export', 'run_gen_export_wizard')( - output, - default_env=default_env, - default_table_names=default_table_names, - default_mode=default_mode, - default_output_file=default_output_file, - default_dry_run=default_dry_run, - ) - - def _register_gen_import_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `gen-import` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def gen_import( - output: OutputOption = 'text', - default_env: str = typer.Option('dev', '--default-env', help='向导默认环境'), - default_table_names: str = typer.Option('', '--default-table-names', help='向导默认物理表名称列表'), - default_dry_run: bool = typer.Option( - True, '--default-dry-run/--no-default-dry-run', help='向导默认 dry-run 选项' - ), - ) -> None: - """ - 通过交互方式执行物理表导入。 - - :param output: 输出格式 - :param default_env: 向导默认环境 - :param default_table_names: 向导默认物理表名称列表 - :param default_dry_run: 向导默认 dry-run 选项 - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.gen_import', 'run_gen_import_wizard')( - output, - default_env=default_env, - default_table_names=default_table_names, - default_dry_run=default_dry_run, - ) - - def _register_prod_check_command(self, app: typer.Typer, registration: WizardCommandRegistration) -> None: - """ - 注册 `prod-check` 子命令。 - - :param app: wizard 子应用 - :param registration: 子命令注册元数据 - :return: None - """ - - @app.command(registration.name, help=registration.help_text) - def prod_check( - output: OutputOption = 'text', - default_env: str = typer.Option('prod', '--default-env', help='向导默认环境'), - default_include_config: bool = typer.Option( - True, - '--default-include-config/--no-default-include-config', - help='向导默认是否附带配置快照', - ), - ) -> None: - """ - 通过交互方式执行生产巡检。 - - :param output: 输出格式 - :param default_env: 向导默认环境 - :param default_include_config: 向导默认是否附带配置快照 - :return: None - """ - self.flow_loader.load_runner('cli.wizard.flows.prod_check', 'run_prod_check_wizard')( - output, - default_env=default_env, - default_include_config=default_include_config, - ) - - -WIZARD_COMMAND_BUILDER = WizardCommandBuilder() diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/__init__.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/app_run.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/app_run.py deleted file mode 100644 index f04c910..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/app_run.py +++ /dev/null @@ -1,152 +0,0 @@ -from dataclasses import dataclass -from typing import Any - -from cli.context import CliContext -from cli.wizard.base import BaseLiveExecCliWizardFlow - - -@dataclass(frozen=True) -class AppRunWizardSelection: - """ - `wizard app-run` 向导采集结果。 - - :param env: 运行环境 - :param run_doctor: 启动前是否先执行应用检查 - """ - - env: str - run_doctor: bool - - -class AppRunWizardFlow(BaseLiveExecCliWizardFlow[AppRunWizardSelection]): - """ - `wizard app-run` 向导流程。 - - :param doctor_payload: 启动前检查结果缓存 - """ - - wizard_name = 'wizard app-run' - preview_title = 'wizard app-run preview' - failure_message = '应用启动向导执行失败' - - def collect_selection(self) -> AppRunWizardSelection: - """ - 采集应用启动向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env('dev') - run_doctor = self.prompt_service.prompt_confirm('启动前是否先执行应用检查', default_value=True) - return AppRunWizardSelection(env=env, run_doctor=run_doctor) - - def prepare_context(self, selection: AppRunWizardSelection, output: str) -> CliContext: - """ - 构建应用启动向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - del output - return self.build_readonly_context(selection.env, 'text') - - def build_preview_summary(self, selection: AppRunWizardSelection) -> dict[str, Any]: - """ - 构建应用启动预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'run_doctor': str(selection.run_doctor).lower(), - 'doctor_ok': '-' if self.doctor_payload is None else str(self.doctor_payload.get('ok', False)).lower(), - } - - def build_preview_command(self, selection: AppRunWizardSelection) -> list[str]: - """ - 构建应用启动预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - return ['shierfeng', 'app', 'run', f'--env={selection.env}'] - - def build_preview_notes(self, selection: AppRunWizardSelection) -> list[str] | None: - """ - 构建应用启动预览附加说明。 - - :param selection: 向导采集结果 - :return: 预览附加说明 - """ - del selection - notes: list[str] = [] - if isinstance(self.doctor_payload, dict) and not self.doctor_payload.get('ok', False): - notes.append(self.doctor_payload.get('message', '启动前检查未通过')) - return notes or None - - def confirm_prompt(self, selection: AppRunWizardSelection) -> str: - """ - 返回应用启动最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认启动应用吗' - - def confirm_default_value(self, selection: AppRunWizardSelection) -> bool: - """ - 返回应用启动确认默认值。 - - :param selection: 向导采集结果 - :return: 默认确认值 - """ - del selection - return bool(self.doctor_payload is None or self.doctor_payload.get('ok', False)) - - def build_execute_arguments(self, selection: AppRunWizardSelection, output: str) -> list[str]: - """ - 构建应用启动内部 CLI 参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - del output - return ['app', 'run', f'--env={selection.env}'] - - def __init__(self) -> None: - """ - 初始化应用启动向导流程。 - - :return: None - """ - super().__init__() - self.doctor_payload: dict[str, Any] | None = None - - def prepare_execution_state(self, selection: AppRunWizardSelection, output: str) -> None: - """ - 在预览与确认前准备启动前检查结果。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: None - """ - del output - self.doctor_payload = None - if selection.run_doctor: - doctor_result = self.run_nested_command( - 'app', 'doctor', f'--env={selection.env}', '--output=json', parse_json=True - ) - payload = getattr(doctor_result, 'payload', None) - self.doctor_payload = payload if isinstance(payload, dict) else None - - -def run_app_run_wizard() -> None: - """ - 执行 `wizard app-run` 向导。 - - :return: None - """ - AppRunWizardFlow().run('text') diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/cache_clear.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/cache_clear.py deleted file mode 100644 index 3d5e806..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/cache_clear.py +++ /dev/null @@ -1,213 +0,0 @@ -from dataclasses import dataclass - -from cli.context import CliContext, OutputOption -from cli.wizard.base import BaseCliWizardFlow - - -@dataclass(frozen=True) -class CacheClearWizardSelection: - """ - `wizard cache-clear` 向导采集结果。 - - :param env: 运行环境 - :param mode: 清理模式 - :param cache_name: 缓存名称前缀 - :param cache_key: 缓存键名关键字 - :param dry_run: 是否演练执行 - :param allow_prod: 是否允许生产环境执行 - """ - - env: str - mode: str - cache_name: str - cache_key: str - dry_run: bool - allow_prod: bool - - -class CacheClearWizardFlow(BaseCliWizardFlow[CacheClearWizardSelection]): - """ - `wizard cache-clear` 向导流程。 - - :param default_env: 默认环境 - :param default_mode: 默认清理模式 - :param default_cache_name: 默认缓存名称前缀 - :param default_cache_key: 默认缓存键名关键字 - :param default_dry_run: 默认是否先执行 dry-run - """ - - wizard_name = 'wizard cache-clear' - preview_title = 'wizard cache-clear preview' - failure_message = '缓存清理向导执行失败' - - def __init__( - self, - *, - default_env: str = 'dev', - default_mode: str = 'cache-name', - default_cache_name: str = '', - default_cache_key: str = '', - default_dry_run: bool = True, - ) -> None: - """ - 初始化缓存清理向导流程。 - - :param default_env: 默认环境 - :param default_mode: 默认清理模式 - :param default_cache_name: 默认缓存名称前缀 - :param default_cache_key: 默认缓存键名关键字 - :param default_dry_run: 默认是否先执行 dry-run - :return: None - """ - self.default_env = default_env - self.default_mode = default_mode - self.default_cache_name = default_cache_name - self.default_cache_key = default_cache_key - self.default_dry_run = default_dry_run - - def collect_selection(self) -> CacheClearWizardSelection: - """ - 采集缓存清理向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env(self.default_env) - mode = self.prompt_service.prompt_choice( - '清理模式', - ['cache-name', 'cache-key', 'all'], - self.default_mode, - ) - cache_name = '' - cache_key = '' - if mode == 'cache-name': - cache_name = self.prompt_service.prompt_required_text('缓存名称前缀', self.default_cache_name) - elif mode == 'cache-key': - cache_key = self.prompt_service.prompt_required_text('缓存键名关键字', self.default_cache_key) - else: - self.prompt_service.prompt_optional_text('当前将清理全部缓存,按回车继续', '') - dry_run = self.prompt_service.prompt_confirm('是否先执行 dry-run 预演', default_value=self.default_dry_run) - allow_prod = ( - self.prompt_service.prompt_confirm('当前为 prod 环境,是否允许继续执行', default_value=False) - if env == 'prod' - else False - ) - return CacheClearWizardSelection( - env=env, - mode=mode, - cache_name=cache_name, - cache_key=cache_key, - dry_run=dry_run, - allow_prod=allow_prod, - ) - - def prepare_context(self, selection: CacheClearWizardSelection, output: OutputOption) -> CliContext: - """ - 构建缓存清理向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_regular_context( - selection.env, - output, - allow_prod=selection.allow_prod, - yes=True, - dry_run=selection.dry_run, - ) - - def build_preview_summary(self, selection: CacheClearWizardSelection) -> dict[str, str]: - """ - 构建缓存清理预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'mode': selection.mode, - 'cache_name': selection.cache_name or '-', - 'cache_key': selection.cache_key or '-', - 'dry_run': str(selection.dry_run).lower(), - 'allow_prod': str(selection.allow_prod).lower(), - } - - def build_preview_command(self, selection: CacheClearWizardSelection) -> list[str]: - """ - 构建缓存清理预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - command = ['shierfeng', 'cache', 'clear', f'--env={selection.env}'] - if selection.mode == 'cache-name': - command.append(f'--cache-name={selection.cache_name}') - elif selection.mode == 'cache-key': - command.append(f'--cache-key={selection.cache_key}') - else: - command.append('--all') - if selection.dry_run: - command.append('--dry-run') - if selection.allow_prod: - command.append('--allow-prod') - return command - - def confirm_prompt(self, selection: CacheClearWizardSelection) -> str: - """ - 返回缓存清理最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认执行缓存清理向导吗' - - def build_execute_arguments(self, selection: CacheClearWizardSelection, output: OutputOption) -> list[str]: - """ - 构建缓存清理内部 CLI 参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - arguments = ['cache', 'clear', f'--env={selection.env}', f'--output={output}', '--yes'] - if selection.mode == 'cache-name': - arguments.append(f'--cache-name={selection.cache_name}') - elif selection.mode == 'cache-key': - arguments.append(f'--cache-key={selection.cache_key}') - else: - arguments.append('--all') - if selection.dry_run: - arguments.append('--dry-run') - if selection.allow_prod: - arguments.append('--allow-prod') - return arguments - - -def run_cache_clear_wizard( - output: OutputOption = 'text', - *, - default_env: str = 'dev', - default_mode: str = 'cache-name', - default_cache_name: str = '', - default_cache_key: str = '', - default_dry_run: bool = True, -) -> None: - """ - 执行 `wizard cache-clear` 向导。 - - :param output: 输出格式 - :param default_env: 默认环境 - :param default_mode: 默认清理模式 - :param default_cache_name: 默认缓存名称前缀 - :param default_cache_key: 默认缓存键关键字 - :param default_dry_run: 默认是否执行 dry-run - :return: None - """ - CacheClearWizardFlow( - default_env=default_env, - default_mode=default_mode, - default_cache_name=default_cache_name, - default_cache_key=default_cache_key, - default_dry_run=default_dry_run, - ).run(output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/db_upgrade.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/db_upgrade.py deleted file mode 100644 index 1f9c3b2..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/db_upgrade.py +++ /dev/null @@ -1,169 +0,0 @@ -from dataclasses import dataclass - -from cli.context import CliContext, OutputOption -from cli.wizard.base import BaseCliWizardFlow - - -@dataclass(frozen=True) -class DbUpgradeWizardSelection: - """ - `wizard db-upgrade` 向导采集结果。 - - :param env: 运行环境 - :param revision: 目标迁移版本 - :param dry_run: 是否演练执行 - :param allow_prod: 是否允许生产环境执行 - """ - - env: str - revision: str - dry_run: bool - allow_prod: bool - - -class DbUpgradeWizardFlow(BaseCliWizardFlow[DbUpgradeWizardSelection]): - """ - `wizard db-upgrade` 向导流程。 - - :param default_env: 默认环境 - :param default_revision: 默认目标 revision - :param default_dry_run: 默认是否先执行 dry-run - """ - - wizard_name = 'wizard db-upgrade' - preview_title = 'wizard db-upgrade preview' - failure_message = '数据库升级向导执行失败' - - def __init__( - self, - *, - default_env: str = 'dev', - default_revision: str = 'head', - default_dry_run: bool = True, - ) -> None: - """ - 初始化数据库升级向导流程。 - - :param default_env: 默认环境 - :param default_revision: 默认目标 revision - :param default_dry_run: 默认是否先执行 dry-run - :return: None - """ - self.default_env = default_env - self.default_revision = default_revision - self.default_dry_run = default_dry_run - - def collect_selection(self) -> DbUpgradeWizardSelection: - """ - 采集数据库升级向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env(self.default_env) - revision = self.prompt_service.prompt_required_text('目标迁移版本', self.default_revision) - dry_run = self.prompt_service.prompt_confirm('是否先执行 dry-run 预演', default_value=self.default_dry_run) - allow_prod = ( - self.prompt_service.prompt_confirm('当前为 prod 环境,是否允许继续执行', default_value=False) - if env == 'prod' - else False - ) - return DbUpgradeWizardSelection(env=env, revision=revision, dry_run=dry_run, allow_prod=allow_prod) - - def prepare_context(self, selection: DbUpgradeWizardSelection, output: OutputOption) -> CliContext: - """ - 构建数据库升级向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_regular_context( - selection.env, - output, - allow_prod=selection.allow_prod, - yes=True, - dry_run=selection.dry_run, - ) - - def build_preview_summary(self, selection: DbUpgradeWizardSelection) -> dict[str, str]: - """ - 构建数据库升级预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'revision': selection.revision, - 'dry_run': str(selection.dry_run).lower(), - 'allow_prod': str(selection.allow_prod).lower(), - } - - def build_preview_command(self, selection: DbUpgradeWizardSelection) -> list[str]: - """ - 构建数据库升级预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - command = ['shierfeng', 'db', 'upgrade', f'--env={selection.env}', f'--revision={selection.revision}'] - if selection.dry_run: - command.append('--dry-run') - if selection.allow_prod: - command.append('--allow-prod') - return command - - def confirm_prompt(self, selection: DbUpgradeWizardSelection) -> str: - """ - 返回数据库升级最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认执行数据库升级向导吗' - - def build_execute_arguments(self, selection: DbUpgradeWizardSelection, output: OutputOption) -> list[str]: - """ - 构建数据库升级内部 CLI 参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - arguments = [ - 'db', - 'upgrade', - f'--env={selection.env}', - f'--output={output}', - f'--revision={selection.revision}', - '--yes', - ] - if selection.dry_run: - arguments.append('--dry-run') - if selection.allow_prod: - arguments.append('--allow-prod') - return arguments - - -def run_db_upgrade_wizard( - output: OutputOption = 'text', - *, - default_env: str = 'dev', - default_revision: str = 'head', - default_dry_run: bool = True, -) -> None: - """ - 执行 `wizard db-upgrade` 向导。 - - :param output: 输出格式 - :param default_env: 默认环境 - :param default_revision: 默认目标 revision - :param default_dry_run: 默认是否先 dry-run - :return: None - """ - DbUpgradeWizardFlow( - default_env=default_env, - default_revision=default_revision, - default_dry_run=default_dry_run, - ).run(output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_export.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_export.py deleted file mode 100644 index 1a2bf0b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_export.py +++ /dev/null @@ -1,239 +0,0 @@ -from dataclasses import dataclass - -import typer - -from cli.context import CliContext, OutputOption -from cli.wizard.base import BaseCliWizardFlow - - -@dataclass(frozen=True) -class GenExportWizardSelection: - """ - `wizard gen-export` 向导采集结果。 - - :param env: 运行环境 - :param table_names: 业务表名称列表 - :param mode: 导出模式 - :param output_file: zip 导出目标文件路径 - :param dry_run: 是否演练执行 - :param allow_prod: 是否允许生产环境执行 - """ - - env: str - table_names: list[str] - mode: str - output_file: str - dry_run: bool - allow_prod: bool - - -class GenExportWizardFlow(BaseCliWizardFlow[GenExportWizardSelection]): - """ - `wizard gen-export` 向导流程。 - - :param default_env: 默认环境 - :param default_table_names: 默认业务表名称列表文本 - :param default_mode: 默认导出模式 - :param default_output_file: 默认 zip 导出目标文件路径 - :param default_dry_run: 默认是否先执行 dry-run - """ - - wizard_name = 'wizard gen-export' - preview_title = 'wizard gen-export preview' - failure_message = '代码导出向导执行失败' - - def __init__( - self, - *, - default_env: str = 'dev', - default_table_names: str = '', - default_mode: str = 'zip', - default_output_file: str = '', - default_dry_run: bool = True, - ) -> None: - """ - 初始化代码导出向导流程。 - - :param default_env: 默认环境 - :param default_table_names: 默认业务表名称列表文本 - :param default_mode: 默认导出模式 - :param default_output_file: 默认 zip 导出目标文件路径 - :param default_dry_run: 默认是否先执行 dry-run - :return: None - """ - self.default_env = default_env - self.default_table_names = default_table_names - self.default_mode = default_mode - self.default_output_file = default_output_file - self.default_dry_run = default_dry_run - - def collect_selection(self) -> GenExportWizardSelection: - """ - 采集代码导出向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env(self.default_env) - raw_table_names = self.prompt_service.prompt_required_text( - '业务表名称列表(多个表请使用逗号分隔)', - self.default_table_names, - ) - table_names = self.parse_table_names(raw_table_names) - while not table_names: - typer.echo('至少需要输入一个业务表名称,请重新输入。') - raw_table_names = self.prompt_service.prompt_required_text( - '业务表名称列表(多个表请使用逗号分隔)', - self.default_table_names, - ) - table_names = self.parse_table_names(raw_table_names) - - mode = self.prompt_service.prompt_choice('导出模式', ['zip', 'local'], self.default_mode) - output_file = '' - if mode == 'zip': - output_file = self.prompt_service.prompt_optional_text( - 'zip 导出目标文件路径(留空则使用默认文件名)', - self.default_output_file, - ) - dry_run = self.prompt_service.prompt_confirm('是否先执行 dry-run 预演', default_value=self.default_dry_run) - allow_prod = ( - self.prompt_service.prompt_confirm('当前为 prod 环境,是否允许继续执行', default_value=False) - if env == 'prod' - else False - ) - return GenExportWizardSelection( - env=env, - table_names=table_names, - mode=mode, - output_file=output_file, - dry_run=dry_run, - allow_prod=allow_prod, - ) - - def prepare_context(self, selection: GenExportWizardSelection, output: OutputOption) -> CliContext: - """ - 构建代码导出向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_regular_context( - selection.env, - output, - allow_prod=selection.allow_prod, - yes=True, - dry_run=selection.dry_run, - ) - - def build_preview_summary(self, selection: GenExportWizardSelection) -> dict[str, str]: - """ - 构建代码导出预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'table_names': ','.join(selection.table_names), - 'mode': selection.mode, - 'output_file': selection.output_file or '-', - 'dry_run': str(selection.dry_run).lower(), - 'allow_prod': str(selection.allow_prod).lower(), - } - - def build_preview_command(self, selection: GenExportWizardSelection) -> list[str]: - """ - 构建代码导出预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - command = [ - 'shierfeng', - 'gen', - 'export', - *selection.table_names, - f'--env={selection.env}', - f'--mode={selection.mode}', - ] - if selection.output_file: - command.append(f'--output-file={selection.output_file}') - if selection.dry_run: - command.append('--dry-run') - if selection.allow_prod: - command.append('--allow-prod') - return command - - def confirm_prompt(self, selection: GenExportWizardSelection) -> str: - """ - 返回代码导出最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认执行代码导出向导吗' - - def build_execute_arguments(self, selection: GenExportWizardSelection, output: OutputOption) -> list[str]: - """ - 构建代码导出内部 CLI 参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - arguments = [ - 'gen', - 'export', - *selection.table_names, - f'--env={selection.env}', - f'--output={output}', - f'--mode={selection.mode}', - '--yes', - ] - if selection.output_file: - arguments.append(f'--output-file={selection.output_file}') - if selection.dry_run: - arguments.append('--dry-run') - if selection.allow_prod: - arguments.append('--allow-prod') - return arguments - - @staticmethod - def parse_table_names(raw_value: str) -> list[str]: - """ - 将逗号分隔的业务表名称文本解析为列表。 - - :param raw_value: 原始输入文本 - :return: 去空白后的业务表名称列表 - """ - return [table_name.strip() for table_name in raw_value.split(',') if table_name.strip()] - - -def run_gen_export_wizard( - output: OutputOption = 'text', - *, - default_env: str = 'dev', - default_table_names: str = '', - default_mode: str = 'zip', - default_output_file: str = '', - default_dry_run: bool = True, -) -> None: - """ - 执行 `wizard gen-export` 向导。 - - :param output: 输出格式 - :param default_env: 默认环境 - :param default_table_names: 默认业务表名称列表文本 - :param default_mode: 默认导出模式 - :param default_output_file: 默认 zip 导出目标文件路径 - :param default_dry_run: 默认是否先 dry-run - :return: None - """ - GenExportWizardFlow( - default_env=default_env, - default_table_names=default_table_names, - default_mode=default_mode, - default_output_file=default_output_file, - default_dry_run=default_dry_run, - ).run(output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_import.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_import.py deleted file mode 100644 index d57db10..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/gen_import.py +++ /dev/null @@ -1,208 +0,0 @@ -from dataclasses import dataclass - -import typer - -from cli.context import CliContext, OutputOption -from cli.wizard.base import BaseCliWizardFlow - - -@dataclass(frozen=True) -class GenImportWizardSelection: - """ - `wizard gen-import` 向导采集结果。 - - :param env: 运行环境 - :param table_names: 待导入物理表名称列表 - :param dry_run: 是否演练执行 - :param allow_prod: 是否允许生产环境执行 - """ - - env: str - table_names: list[str] - dry_run: bool - allow_prod: bool - - -class GenImportWizardFlow(BaseCliWizardFlow[GenImportWizardSelection]): - """ - `wizard gen-import` 向导流程。 - - :param default_env: 默认环境 - :param default_table_names: 默认物理表名称列表文本 - :param default_dry_run: 默认是否先执行 dry-run - """ - - wizard_name = 'wizard gen-import' - preview_title = 'wizard gen-import preview' - failure_message = '代码生成导入向导执行失败' - - def __init__( - self, - *, - default_env: str = 'dev', - default_table_names: str = '', - default_dry_run: bool = True, - ) -> None: - """ - 初始化代码生成导入向导流程。 - - :param default_env: 默认环境 - :param default_table_names: 默认物理表名称列表文本 - :param default_dry_run: 默认是否先执行 dry-run - :return: None - """ - self.default_env = default_env - self.default_table_names = default_table_names - self.default_dry_run = default_dry_run - - def collect_selection(self) -> GenImportWizardSelection: - """ - 采集代码生成导入向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env(self.default_env) - raw_table_names = self.prompt_service.prompt_required_text( - '物理表名称列表(多个表请使用逗号分隔)', - self.default_table_names, - ) - table_names = self.parse_table_names(raw_table_names) - while not table_names: - typer.echo('至少需要输入一个物理表名称,请重新输入。') - raw_table_names = self.prompt_service.prompt_required_text( - '物理表名称列表(多个表请使用逗号分隔)', - self.default_table_names, - ) - table_names = self.parse_table_names(raw_table_names) - - dry_run = self.prompt_service.prompt_confirm('是否先执行 dry-run 预演', default_value=self.default_dry_run) - allow_prod = ( - self.prompt_service.prompt_confirm('当前为 prod 环境,是否允许继续执行', default_value=False) - if env == 'prod' - else False - ) - return GenImportWizardSelection( - env=env, - table_names=table_names, - dry_run=dry_run, - allow_prod=allow_prod, - ) - - def prepare_context(self, selection: GenImportWizardSelection, output: OutputOption) -> CliContext: - """ - 构建代码生成导入向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_regular_context( - selection.env, - output, - allow_prod=selection.allow_prod, - yes=True, - dry_run=selection.dry_run, - ) - - def build_preview_summary(self, selection: GenImportWizardSelection) -> dict[str, str]: - """ - 构建代码生成导入预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'table_names': ','.join(selection.table_names), - 'dry_run': str(selection.dry_run).lower(), - 'allow_prod': str(selection.allow_prod).lower(), - } - - def build_preview_command(self, selection: GenImportWizardSelection) -> list[str]: - """ - 构建代码生成导入预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - command = ['shierfeng', 'gen', 'import-table', *selection.table_names, f'--env={selection.env}'] - if selection.dry_run: - command.append('--dry-run') - if selection.allow_prod: - command.append('--allow-prod') - return command - - def build_preview_notes(self, selection: GenImportWizardSelection) -> list[str] | None: - """ - 构建代码生成导入预览附加说明。 - - :param selection: 向导采集结果 - :return: 预览附加说明 - """ - del selection - return ['建议先确认物理表注释和字段规模,再决定是否执行真实导入。'] - - def confirm_prompt(self, selection: GenImportWizardSelection) -> str: - """ - 返回代码生成导入最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认执行代码生成导入向导吗' - - def build_execute_arguments(self, selection: GenImportWizardSelection, output: OutputOption) -> list[str]: - """ - 构建代码生成导入内部 CLI 参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - arguments = [ - 'gen', - 'import-table', - *selection.table_names, - f'--env={selection.env}', - f'--output={output}', - '--yes', - ] - if selection.dry_run: - arguments.append('--dry-run') - if selection.allow_prod: - arguments.append('--allow-prod') - return arguments - - @staticmethod - def parse_table_names(raw_value: str) -> list[str]: - """ - 将逗号分隔的物理表名称文本解析为列表。 - - :param raw_value: 原始输入文本 - :return: 去空白后的物理表名称列表 - """ - return [table_name.strip() for table_name in raw_value.split(',') if table_name.strip()] - - -def run_gen_import_wizard( - output: OutputOption = 'text', - *, - default_env: str = 'dev', - default_table_names: str = '', - default_dry_run: bool = True, -) -> None: - """ - 执行 `wizard gen-import` 向导。 - - :param output: 输出格式 - :param default_env: 默认环境 - :param default_table_names: 默认物理表名称列表文本 - :param default_dry_run: 默认是否先 dry-run - :return: None - """ - GenImportWizardFlow( - default_env=default_env, - default_table_names=default_table_names, - default_dry_run=default_dry_run, - ).run(output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/prod_check.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/prod_check.py deleted file mode 100644 index 8071296..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/flows/prod_check.py +++ /dev/null @@ -1,224 +0,0 @@ -from dataclasses import dataclass -from typing import Any - -from cli.context import CliContext, OutputOption -from cli.wizard.aggregators import ProdCheckAggregator -from cli.wizard.base import BaseCliWizardFlow -from cli.wizard.presenters import ProdCheckPresenter - - -@dataclass(frozen=True) -class ProdCheckWizardSelection: - """ - `wizard prod-check` 向导采集结果。 - - :param env: 运行环境 - :param include_config: 是否附带配置快照 - """ - - env: str - include_config: bool - - -class ProdCheckWizardFlow(BaseCliWizardFlow[ProdCheckWizardSelection]): - """ - `wizard prod-check` 向导流程。 - - :param default_env: 默认环境 - :param default_include_config: 默认是否附带配置快照 - """ - - wizard_name = 'wizard prod-check' - preview_title = 'wizard prod-check preview' - failure_message = '生产巡检向导执行失败' - - def __init__( - self, - *, - default_env: str = 'prod', - default_include_config: bool = True, - ) -> None: - """ - 初始化生产巡检向导流程。 - - :param default_env: 默认环境 - :param default_include_config: 默认是否附带配置快照 - :return: None - """ - super().__init__() - self.default_env = default_env - self.default_include_config = default_include_config - self.aggregator = ProdCheckAggregator() - self.presenter = ProdCheckPresenter() - - def collect_selection(self) -> ProdCheckWizardSelection: - """ - 采集生产巡检向导参数。 - - :return: 向导采集结果 - """ - env = self.prompt_service.prompt_env(self.default_env) - include_config = self.prompt_service.prompt_confirm( - '是否附带应用配置快照', - default_value=self.default_include_config, - ) - return ProdCheckWizardSelection(env=env, include_config=include_config) - - def prepare_context(self, selection: ProdCheckWizardSelection, output: OutputOption) -> CliContext: - """ - 构建生产巡检向导上下文。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: CLI 上下文 - """ - return self.build_readonly_context(selection.env, output) - - def build_preview_summary(self, selection: ProdCheckWizardSelection) -> dict[str, str]: - """ - 构建生产巡检预览摘要。 - - :param selection: 向导采集结果 - :return: 预览摘要 - """ - return { - 'env': selection.env, - 'include_config': str(selection.include_config).lower(), - } - - def build_preview_command(self, selection: ProdCheckWizardSelection) -> list[str]: - """ - 构建生产巡检预览命令。 - - :param selection: 向导采集结果 - :return: 用户视角命令参数 - """ - return ['shierfeng', 'app', 'doctor', f'--env={selection.env}'] - - def confirm_prompt(self, selection: ProdCheckWizardSelection) -> str: - """ - 返回生产巡检最终确认提示。 - - :param selection: 向导采集结果 - :return: 确认提示 - """ - del selection - return '确认执行生产巡检向导吗' - - def confirm_default_value(self, selection: ProdCheckWizardSelection) -> bool: - """ - 返回生产巡检确认默认值。 - - :param selection: 向导采集结果 - :return: 默认确认值 - """ - del selection - return True - - def build_execute_arguments(self, selection: ProdCheckWizardSelection, output: OutputOption) -> list[str]: - """ - 返回生产巡检占位执行参数。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 内部 CLI 参数 - """ - del selection, output - return [] - - def execute(self, selection: ProdCheckWizardSelection, output: OutputOption) -> dict[str, Any]: - """ - 执行生产巡检聚合逻辑。 - - :param selection: 向导采集结果 - :param output: 输出格式 - :return: 聚合后的巡检结果 - """ - del output - runtime_result = self.run_nested_command( - 'app', 'env', f'--env={selection.env}', '--output=json', parse_json=True - ) - doctor_result = self.run_nested_command( - 'app', - 'doctor', - f'--env={selection.env}', - '--output=json', - parse_json=True, - ) - config_payload = None - if selection.include_config: - config_result = self.run_nested_command( - 'app', - 'config', - f'--env={selection.env}', - '--output=json', - parse_json=True, - ) - config_payload = config_result.payload - return self.aggregator.build_payload( - env=selection.env, - runtime_payload=runtime_result.payload if isinstance(runtime_result.payload, dict) else None, - doctor_payload=doctor_result.payload if isinstance(doctor_result.payload, dict) else None, - config_payload=config_payload, - ) - - def complete_payload( - self, - ctx: CliContext, - payload: dict[str, Any], - *, - default_exit_code: int = 0, - ) -> None: - """ - 以聚合结果展示器收口生产巡检结果。 - - :param ctx: CLI 上下文 - :param payload: 聚合结果负载 - :param default_exit_code: 默认退出码 - :return: None - """ - self.complete_payload_result( - ctx, - payload, - text_builder=self.presenter.build_text, - default_exit_code=default_exit_code, - ) - - def extract_payload(self, nested_result: Any) -> dict[str, Any] | None: - """ - 提取生产巡检聚合结果负载。 - - :param nested_result: 聚合执行结果 - :return: 结构化结果负载 - """ - return nested_result if isinstance(nested_result, dict) else None - - def extract_returncode(self, nested_result: Any) -> int: - """ - 提取生产巡检聚合结果退出码。 - - :param nested_result: 聚合执行结果 - :return: 退出码 - """ - del nested_result - return 0 - - -def run_prod_check_wizard( - output: OutputOption = 'text', - *, - default_env: str = 'prod', - default_include_config: bool = True, -) -> None: - """ - 执行 `wizard prod-check` 向导。 - - :param output: 输出格式 - :param default_env: 默认环境 - :param default_include_config: 默认是否附带配置快照 - :return: None - """ - ProdCheckWizardFlow( - default_env=default_env, - default_include_config=default_include_config, - ).run(output) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/presenters.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/presenters.py deleted file mode 100644 index a0eea0b..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/presenters.py +++ /dev/null @@ -1,95 +0,0 @@ -from dataclasses import dataclass -from typing import Any - - -@dataclass(frozen=True) -class ProdCheckRenderingSupport: - """ - `wizard prod-check` 文本渲染支持对象。 - - 该对象负责构建 runtime、doctor、config 分区文本行, - 避免 presenter 本体继续维护全部分区细节。 - """ - - @staticmethod - def build_runtime_lines(runtime: dict[str, Any]) -> list[str]: - """ - 构建 runtime 分区文本行。 - - :param runtime: runtime 结果片段 - :return: 文本行列表 - """ - return [ - 'runtime:', - f' cli_env: {runtime.get("cliEnv", "-")}', - f' config_env: {runtime.get("configEnv", "-")}', - f' env_file: {runtime.get("envFile", "-")}', - f' env_file_exists: {str(runtime.get("envFileExists", False)).lower()}', - ] - - @staticmethod - def build_doctor_lines(doctor: dict[str, Any]) -> list[str]: - """ - 构建 doctor 分区文本行。 - - :param doctor: doctor 结果片段 - :return: 文本行列表 - """ - return [ - 'doctor:', - f' ok: {str(doctor.get("ok", False)).lower()}', - f' message: {doctor.get("message", "-")}', - ] - - @staticmethod - def build_config_lines(config: dict[str, Any]) -> list[str]: - """ - 构建 config 分区文本行。 - - :param config: config 结果片段 - :return: 文本行列表 - """ - return [ - 'config:', - f' name: {config.get("name", "-")}', - f' host: {config.get("host", "-")}:{config.get("port", "-")}', - f' db_type: {config.get("dbType", "-")}', - f' redis_host: {config.get("redisHost", "-")}:{config.get("redisPort", "-")}', - ] - - -@dataclass(frozen=True) -class ProdCheckPresenter: - """ - `wizard prod-check` 结果展示器。 - - 该对象负责将聚合结果转换为文本摘要, - 避免在 flow 内继续维护文本渲染细节。 - - :param rendering_support: 文本渲染支持对象 - """ - - rendering_support: ProdCheckRenderingSupport = ProdCheckRenderingSupport() - - def build_text(self, payload: dict[str, Any]) -> str: - """ - 将生产巡检聚合结果渲染为文本摘要。 - - :param payload: 聚合结果字典 - :return: 文本摘要 - """ - lines = [ - f'ok: {str(payload.get("ok", False)).lower()}', - f'env: {payload.get("env", "-")}', - f'message: {payload.get("message", "-")}', - ] - runtime = payload.get('runtime') - if isinstance(runtime, dict): - lines.extend(self.rendering_support.build_runtime_lines(runtime)) - doctor = payload.get('doctor') - if isinstance(doctor, dict): - lines.extend(self.rendering_support.build_doctor_lines(doctor)) - config = payload.get('config') - if isinstance(config, dict): - lines.extend(self.rendering_support.build_config_lines(config)) - return '\n'.join(lines) diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/preview.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/preview.py deleted file mode 100644 index 96b31f7..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/preview.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Any - -from cli.exit_codes import GUARD_REJECTED -from cli.output import CommandResult -from cli.utils import SHELL_TEXT_FORMATTER - - -class WizardPreviewRenderer: - """ - 向导预览渲染器。 - - 该对象负责统一构建向导预览文本和取消结果, - 为各个 flow 提供稳定的类式渲染入口。 - """ - - @staticmethod - def render_preview( - title: str, - *, - summary: dict[str, Any], - command: list[str], - notes: list[str] | None = None, - ) -> str: - """ - 渲染向导执行预览文本。 - - :param title: 预览标题 - :param summary: 关键参数摘要 - :param command: 用户视角命令参数列表 - :param notes: 额外提示列表 - :return: 可直接输出的预览文本 - """ - lines = [title, 'summary:'] - for key, value in summary.items(): - lines.append(f' {key}: {value}') - lines.extend( - [ - 'command:', - f' {SHELL_TEXT_FORMATTER.format_shell_command(command)}', - ] - ) - if notes: - lines.append('notes:') - lines.extend(f' - {note}' for note in notes if note) - return '\n'.join(lines) - - @staticmethod - def build_cancel_result(wizard_name: str) -> CommandResult: - """ - 构建向导取消结果。 - - :param wizard_name: 向导名称 - :return: 命令结果对象 - """ - return CommandResult( - data={ - 'ok': False, - 'message': f'已取消向导执行:{wizard_name}', - 'hint': '可重新运行向导并调整参数后再次执行', - }, - exit_code=GUARD_REJECTED, - ) - - -WIZARD_PREVIEW_RENDERER = WizardPreviewRenderer() diff --git a/shierfeng-fastapi-backend/build/lib/cli/wizard/prompts.py b/shierfeng-fastapi-backend/build/lib/cli/wizard/prompts.py deleted file mode 100644 index 004ae76..0000000 --- a/shierfeng-fastapi-backend/build/lib/cli/wizard/prompts.py +++ /dev/null @@ -1,86 +0,0 @@ -import typer - -from cli.metadata import ENVIRONMENT_OPTION_SERVICE - - -class WizardPromptService: - """ - 向导交互输入服务。 - - 该对象集中封装向导中稳定复用的环境选择、枚举选择、 - 文本采集和确认交互逻辑,避免继续扩散模块级散函数入口。 - """ - - def prompt_env(self, default_env: str = 'dev') -> str: - """ - 交互采集运行环境名称。 - - :param default_env: 默认环境名称 - :return: 用户选择的环境名称 - """ - env_names = ENVIRONMENT_OPTION_SERVICE.discover_env_names() - prompt_message = f'运行环境(可选值: {", ".join(env_names)})' - while True: - env_value = typer.prompt(prompt_message, default=default_env).strip() - if env_value in env_names: - return env_value - typer.echo(f'不支持的环境名称:{env_value},请重新输入。') - - @staticmethod - def prompt_choice(prompt_text: str, choices: list[str], default_value: str) -> str: - """ - 交互采集枚举型文本输入。 - - :param prompt_text: 提示文案 - :param choices: 可选值列表 - :param default_value: 默认值 - :return: 用户选择的枚举值 - """ - while True: - value = typer.prompt( - f'{prompt_text}(可选值: {", ".join(choices)})', - default=default_value, - ).strip() - if value in choices: - return value - typer.echo(f'不支持的选项:{value},请重新输入。') - - @staticmethod - def prompt_required_text(prompt_text: str, default_value: str = '') -> str: - """ - 交互采集必填文本输入。 - - :param prompt_text: 提示文案 - :param default_value: 默认值 - :return: 用户输入文本 - """ - while True: - value = typer.prompt(prompt_text, default=default_value).strip() - if value: - return value - typer.echo('该字段不能为空,请重新输入。') - - @staticmethod - def prompt_optional_text(prompt_text: str, default_value: str = '') -> str: - """ - 交互采集可选文本输入。 - - :param prompt_text: 提示文案 - :param default_value: 默认值 - :return: 用户输入文本 - """ - return typer.prompt(prompt_text, default=default_value).strip() - - @staticmethod - def prompt_confirm(prompt_text: str, default_value: bool = False) -> bool: - """ - 交互采集确认型输入。 - - :param prompt_text: 提示文案 - :param default_value: 默认值 - :return: 用户确认结果 - """ - return typer.confirm(prompt_text, default=default_value) - - -WIZARD_PROMPT_SERVICE = WizardPromptService()