初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,102 @@
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,
)
@@ -0,0 +1,236 @@
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]))
@@ -0,0 +1,210 @@
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}'
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
+163
View File
@@ -0,0 +1,163 @@
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)
+183
View File
@@ -0,0 +1,183 @@
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())
)
+150
View File
@@ -0,0 +1,150 @@
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 '-'
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,173 @@
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)
@@ -0,0 +1,225 @@
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,
)
@@ -0,0 +1,180 @@
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]]
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,94 @@
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)
@@ -0,0 +1,125 @@
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),
)
@@ -0,0 +1,79 @@
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()]]
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,191 @@
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)
@@ -0,0 +1,237 @@
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,
)
@@ -0,0 +1,71 @@
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", "-")}',
]
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,74 @@
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,
)
@@ -0,0 +1,103 @@
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,
)
@@ -0,0 +1,96 @@
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 '-'
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,273 @@
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)
@@ -0,0 +1,304 @@
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)
@@ -0,0 +1,231 @@
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", "-")}',
]
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,216 @@
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)
@@ -0,0 +1,221 @@
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()),
)
@@ -0,0 +1,197 @@
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", "-")}',
]
@@ -0,0 +1,3 @@
from .command import app
__all__ = ['app']
@@ -0,0 +1,89 @@
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)
@@ -0,0 +1,137 @@
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),
)
@@ -0,0 +1,177 @@
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}'