调整部分前端布局和后端增删改逻辑

This commit is contained in:
焦龙言
2026-09-20 10:38:10 +08:00
parent acddc8a237
commit 3edba99d2d
227 changed files with 122871 additions and 839 deletions
@@ -0,0 +1,51 @@
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',
]
@@ -0,0 +1,363 @@
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()
@@ -0,0 +1,121 @@
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()
@@ -0,0 +1,239 @@
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)
@@ -0,0 +1,137 @@
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)