mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-04 17:14:23 +00:00
280 lines
12 KiB
Python
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
|