Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/cli/tui/actions/execution.py
T

280 lines
12 KiB
Python

import asyncio
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
from cli.completion.installers import COMPLETION_INSTALLER, CompletionInstallerService
from cli.core.app_builder import CliApplicationBuilder
from cli.exit_codes import RUNTIME_ERROR
from cli.main import CLI_APPLICATION_BUILDER
from cli.runtime.cache import CACHE_RUNTIME, CacheRuntimeService
from cli.runtime.config import CONFIG_RUNTIME, ConfigRuntimeService
from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService
from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService
from cli.runtime.gen import GEN_RUNTIME, GenRuntimeService
from cli.runtime.job import JOB_RUNTIME, JobRuntimeService
from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService
from cli.tui.actions.models import TuiActionResult, TuiActionSpec
from cli.tui.copy import TUI_COPY
from cli.utils import SHELL_TEXT_FORMATTER
TUI_ACTION_TIMEOUT_SECONDS = 30.0
@dataclass
class TuiActionExecutionService:
"""
TUI 动作执行服务。
该对象将 TUI 动作直接分发到当前进程的 runtime facade,并统一
施加超时、异常收口和结果文本渲染。动作执行不再启动嵌套 CLI。
"""
job_runtime: JobRuntimeService = field(default_factory=lambda: JOB_RUNTIME)
operations_runtime: OperationsRuntimeService = field(default_factory=lambda: OPERATIONS_RUNTIME)
config_runtime: ConfigRuntimeService = field(default_factory=lambda: CONFIG_RUNTIME)
cache_runtime: CacheRuntimeService = field(default_factory=lambda: CACHE_RUNTIME)
gen_runtime: GenRuntimeService = field(default_factory=lambda: GEN_RUNTIME)
database_runtime: DatabaseRuntimeService = field(default_factory=lambda: DATABASE_RUNTIME)
crypto_runtime: CryptoRuntimeService = field(default_factory=lambda: CRYPTO_RUNTIME)
completion_installer: CompletionInstallerService = field(default_factory=lambda: COMPLETION_INSTALLER)
application_builder: CliApplicationBuilder = field(default_factory=lambda: CLI_APPLICATION_BUILDER)
async def execute(self, spec: TuiActionSpec, env: str) -> TuiActionResult:
"""
执行指定 TUI 动作。
:param spec: 动作定义
:param env: 当前运行环境
:return: 动作执行结果
"""
try:
payload = await asyncio.wait_for(
self._dispatch(spec, env),
timeout=TUI_ACTION_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError:
payload = {
'ok': False,
'message': f'{spec.label}执行超时',
'error': f'动作未在 {TUI_ACTION_TIMEOUT_SECONDS:g} 秒内完成',
'exit_code': RUNTIME_ERROR,
}
except asyncio.CancelledError:
raise
except Exception as exc:
payload = {
'ok': False,
'message': f'{spec.label}执行失败',
'error': str(exc) or exc.__class__.__name__,
'exit_code': RUNTIME_ERROR,
}
if isinstance(payload, dict):
payload.setdefault('env', env)
return TuiActionResult(spec=spec, payload=payload)
async def _dispatch(self, spec: TuiActionSpec, env: str) -> dict[str, Any]:
"""
将动作标识分发到对应 runtime。
:param spec: 动作定义
:param env: 当前运行环境
:return: 标准动作结果负载
"""
action_id = spec.action_id
parameters = spec.parameters
if action_id in {'job-run-once', 'job-pause', 'job-resume', 'job-sync'}:
payload = await self._dispatch_job(action_id, parameters)
elif action_id == 'config-sync-cache':
payload = await self.config_runtime.sync_config_cache()
elif action_id in {'cache-warmup', 'cache-clear-dry-run'}:
payload = await self._dispatch_cache(action_id, parameters)
elif action_id in {'gen-export-dry-run', 'gen-sync-db'}:
payload = await self._dispatch_gen(action_id, parameters)
elif action_id in {'db-upgrade-dry-run', 'db-init-dry-run'}:
payload = await self._dispatch_database(action_id, parameters)
elif action_id in {'ops-ping-db', 'ops-ping-redis'}:
payload = await self._dispatch_ops(action_id)
elif action_id == 'app-precheck':
payload = await self._run_precheck('启动前检查')
elif action_id == 'prod-check':
payload = await self._run_precheck('综合运行巡检')
elif action_id == 'completion-install':
payload = await self._install_completion()
elif action_id in {'crypto-keygen', 'crypto-rotate-dry-run'}:
payload = await self._dispatch_crypto(action_id, parameters)
else:
payload = self._unsupported_action(action_id)
return payload
async def _dispatch_job(self, action_id: str, parameters: dict[str, object]) -> dict[str, Any]:
"""执行任务领域动作。"""
job_id = int(parameters.get('job_id') or 0)
if action_id == 'job-run-once':
return await self.job_runtime.run_job_once(job_id)
if action_id == 'job-pause':
return await self.job_runtime.pause_job(job_id)
if action_id == 'job-resume':
return await self.job_runtime.resume_job(job_id)
if action_id == 'job-sync':
return await self.operations_runtime.sync_jobs()
return self._unsupported_action(action_id)
async def _dispatch_cache(self, action_id: str, parameters: dict[str, object]) -> dict[str, Any]:
"""执行缓存领域动作。"""
if action_id == 'cache-warmup':
return await self.cache_runtime.warmup_cache()
if action_id == 'cache-clear-dry-run':
return await self.cache_runtime.clear_cache(
cache_name=str(parameters.get('cache_name') or ''),
dry_run=True,
)
return self._unsupported_action(action_id)
async def _dispatch_gen(self, action_id: str, parameters: dict[str, object]) -> dict[str, Any]:
"""执行代码生成领域动作。"""
table_name = str(parameters.get('table_name') or '')
if action_id == 'gen-export-dry-run':
return await self.gen_runtime.export_code([table_name], mode='zip', dry_run=True)
if action_id == 'gen-sync-db':
return await self.gen_runtime.sync_gen_table_from_db(table_name)
return self._unsupported_action(action_id)
async def _dispatch_database(self, action_id: str, parameters: dict[str, object]) -> dict[str, Any]:
"""执行数据库领域动作。"""
if action_id == 'db-upgrade-dry-run':
return await asyncio.to_thread(
self.database_runtime.upgrade_database,
str(parameters.get('revision') or 'head'),
dry_run=True,
)
if action_id == 'db-init-dry-run':
return await asyncio.to_thread(self.database_runtime.init_database, dry_run=True)
return self._unsupported_action(action_id)
async def _dispatch_ops(self, action_id: str) -> dict[str, Any]:
"""执行运维探活动作。"""
if action_id == 'ops-ping-db':
return await self.database_runtime.ping_database()
if action_id == 'ops-ping-redis':
return await self.operations_runtime.ping_redis()
return self._unsupported_action(action_id)
async def _run_precheck(self, label: str) -> dict[str, Any]:
"""并发执行启动前综合检查。"""
database, redis, crypto, config = await asyncio.gather(
self.database_runtime.ping_database(),
self.operations_runtime.ping_redis(),
asyncio.to_thread(self.crypto_runtime.validate_crypto_config),
self.config_runtime.diagnose_config(sample_limit=5),
)
checks = (database, redis, crypto, config)
checks_ok = all(item.get('ok', False) for item in checks)
return {
'ok': checks_ok,
'message': f'{label}通过' if checks_ok else f'{label}发现异常',
'database': database,
'redis': redis,
'crypto': crypto,
'config': config,
}
async def _install_completion(self) -> dict[str, Any]:
"""在当前进程内安装 shell completion。"""
return await asyncio.to_thread(
self.completion_installer.install_completion_script,
self.application_builder.build(),
None,
activate=True,
)
async def _dispatch_crypto(self, action_id: str, parameters: dict[str, object]) -> dict[str, Any]:
"""执行传输加密领域动作。"""
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
key_size = int(parameters.get('key_size') or 2048)
if action_id == 'crypto-keygen':
return await asyncio.to_thread(
self.crypto_runtime.generate_crypto_key_pair,
f'tui-{timestamp}',
key_size,
)
if action_id == 'crypto-rotate-dry-run':
return await asyncio.to_thread(
self.crypto_runtime.build_rotation_payload,
f'tui-next-{timestamp}',
key_size,
)
return self._unsupported_action(action_id)
@staticmethod
def _unsupported_action(action_id: str) -> dict[str, Any]:
"""构建未知动作的失败结果,禁止隐式回退到其他操作。"""
return {
'ok': False,
'message': f'不支持的 TUI 动作:{action_id}',
'exit_code': RUNTIME_ERROR,
}
def build_result_lines(self, result: TuiActionResult) -> list[str]:
"""
构建动作结果详情文本。
:param result: 动作执行结果
:return: 结果文本行
"""
payload = result.payload if isinstance(result.payload, dict) else {}
lines = [
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('name'),
result.spec.label,
),
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('outcome'),
TUI_COPY.build_action_result_field_label('success')
if result.ok
else TUI_COPY.build_action_result_field_label('fail'),
),
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('summary'),
SHELL_TEXT_FORMATTER.truncate_text(result.message, 88),
),
]
service_message = str(payload.get('serviceMessage', '') or '').strip()
if service_message:
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('service'),
SHELL_TEXT_FORMATTER.truncate_text(service_message, 88),
)
)
if payload.get('hint'):
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('hint'),
SHELL_TEXT_FORMATTER.truncate_text(str(payload.get('hint', '') or ''), 88),
)
)
if payload.get('count') is not None:
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('count'),
str(payload.get('count')),
)
)
if payload.get('jobId') is not None:
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('job_id'),
str(payload.get('jobId')),
)
)
operation_label = str(payload.get('operationLabel', '') or '').strip()
if operation_label and operation_label != result.spec.label:
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('operation'),
SHELL_TEXT_FORMATTER.truncate_text(operation_label, 64),
)
)
return lines