停止跟踪 Python 构建产物并忽略本地环境

This commit is contained in:
2026-10-09 21:13:43 +08:00
parent ae662a4941
commit a62803ccf0
164 changed files with 5 additions and 32352 deletions
@@ -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()
@@ -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()
@@ -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,
)
@@ -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()
@@ -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()
@@ -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)
File diff suppressed because it is too large Load Diff
@@ -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)
@@ -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='仅执行预演,不落地实际变更')]
@@ -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',
]
@@ -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()
@@ -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 原生格式为 `<shell>_<instruction>`,例如 `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 兼容格式为 `<instruction>_<shell>`,例如 `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()
@@ -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)
@@ -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)
@@ -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
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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,
)
@@ -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]))
@@ -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}'
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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())
)
@@ -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 '-'
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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,
)
@@ -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]]
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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),
)
@@ -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()]]
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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,
)
@@ -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", "-")}',
]
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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,
)
@@ -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,
)
@@ -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 '-'
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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)
@@ -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", "-")}',
]
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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()),
)
@@ -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", "-")}',
]
@@ -1,3 +0,0 @@
from .command import app
__all__ = ['app']
@@ -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)
@@ -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),
)
@@ -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}'
@@ -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)
@@ -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()
@@ -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',
]
@@ -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)
@@ -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()
@@ -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)
@@ -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'(?<!^)(?=[A-Z])')
def __init__(self, status_styler: OutputStatusStyler) -> 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)
@@ -1,3 +0,0 @@
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
__all__ = ['RUNTIME_ENVIRONMENT', 'RuntimeEnvironmentService']
@@ -1,3 +0,0 @@
from .service import APP_RUNTIME, AppRuntimeService
__all__ = ['APP_RUNTIME', 'AppRuntimeService']
@@ -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')
@@ -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()
@@ -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(),
}
@@ -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()
@@ -1,3 +0,0 @@
from .service import CACHE_RUNTIME, CacheRuntimeService
__all__ = ['CACHE_RUNTIME', 'CacheRuntimeService']
@@ -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
@@ -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()
@@ -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}'
@@ -1,3 +0,0 @@
from .service import CONFIG_RUNTIME, ConfigRuntimeService
__all__ = ['CONFIG_RUNTIME', 'ConfigRuntimeService']
@@ -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
@@ -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()
@@ -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,
}
@@ -1,3 +0,0 @@
from .service import CRYPTO_RUNTIME, CryptoRuntimeService
__all__ = ['CRYPTO_RUNTIME', 'CryptoRuntimeService']
@@ -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
@@ -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()
@@ -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 后,再移除不再需要的历史密钥',
]
@@ -1,3 +0,0 @@
from .service import DATABASE_RUNTIME, DatabaseRuntimeService
__all__ = ['DATABASE_RUNTIME', 'DatabaseRuntimeService']
@@ -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
@@ -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()
@@ -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}
@@ -1,3 +0,0 @@
from .service import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService
__all__ = ['DEVELOPMENT_RUNTIME', 'DevelopmentRuntimeService']
@@ -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
@@ -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()
@@ -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
@@ -1,3 +0,0 @@
from .service import GEN_RUNTIME, GenRuntimeService
__all__ = ['GEN_RUNTIME', 'GenRuntimeService']
@@ -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
@@ -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()
@@ -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}
@@ -1,3 +0,0 @@
from .service import JOB_RUNTIME, JobRuntimeService
__all__ = ['JOB_RUNTIME', 'JobRuntimeService']
@@ -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
@@ -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()
@@ -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
@@ -1,3 +0,0 @@
from .service import OPERATIONS_RUNTIME, OperationsRuntimeService
__all__ = ['OPERATIONS_RUNTIME', 'OperationsRuntimeService']
@@ -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
@@ -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()
@@ -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,
}
@@ -1,3 +0,0 @@
from cli.tui.commands import TUI_COMMAND_REGISTRATION
__all__ = ['TUI_COMMAND_REGISTRATION']

Some files were not shown because too many files have changed in this diff Show More