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

This commit is contained in:
焦龙言
2026-09-20 10:38:10 +08:00
parent acddc8a237
commit 3edba99d2d
227 changed files with 122871 additions and 839 deletions
@@ -0,0 +1,10 @@
from cli.tui.actions.bootstrap import TUI_ACTION_EXECUTION_SERVICE, TUI_ACTION_PRESENTATION_SERVICE, TUI_ACTION_REGISTRY
from cli.tui.actions.models import TuiActionResult, TuiActionSpec
__all__ = [
'TUI_ACTION_EXECUTION_SERVICE',
'TUI_ACTION_PRESENTATION_SERVICE',
'TUI_ACTION_REGISTRY',
'TuiActionResult',
'TuiActionSpec',
]
@@ -0,0 +1,103 @@
from dataclasses import dataclass
from cli.tui.actions.builders import TuiActionSpecFactory
from cli.tui.actions.factories.cache import CacheActionTemplateFactory
from cli.tui.actions.factories.gen import GenActionTemplateFactory
from cli.tui.actions.factories.jobs import JobActionTemplateFactory
from cli.tui.actions.factories.static import StaticActionTemplateFactory
from cli.tui.actions.registry import TuiActionRegistry, TuiActionSlotResolver
@dataclass(frozen=True)
class TuiActionRegistryBuilder:
"""
TUI 动作注册表构建器。
该对象负责装配各领域动作模板工厂,最终输出浏览页和详情页共用的
动作注册表,避免模板常量继续散落在模块底部。
:param jobs: 任务页动作模板工厂
:param cache: 缓存页动作模板工厂
:param gen: 代码生成页动作模板工厂
:param static: 静态页面动作模板工厂
:param spec_factory: 动作规格构建器
"""
jobs: JobActionTemplateFactory
cache: CacheActionTemplateFactory
gen: GenActionTemplateFactory
static: StaticActionTemplateFactory
spec_factory: TuiActionSpecFactory
def build(self) -> TuiActionRegistry:
"""
构建 TUI 动作注册表。
:return: 动作注册表
"""
return TuiActionRegistry(
browser_resolvers={
'jobs': TuiActionSlotResolver(
slot_templates={
'primary': self.jobs.create_run_once_template(),
'secondary': self.jobs.create_toggle_template(),
'global': self.jobs.create_sync_template(),
},
spec_factory=self.spec_factory,
),
'configs': TuiActionSlotResolver(
slot_templates={
'global': self.static.create_config_sync_template(),
},
spec_factory=self.spec_factory,
),
'cache': TuiActionSlotResolver(
slot_templates={
'global': self.cache.create_clear_wizard_template(),
'utility': self.cache.create_warmup_template(),
},
spec_factory=self.spec_factory,
),
'gen': TuiActionSlotResolver(
slot_templates={
'primary': self.gen.create_export_wizard_template(),
'secondary': self.gen.create_import_wizard_template(),
'global': self.gen.create_export_dry_run_template(),
'utility': self.gen.create_sync_db_template(),
},
spec_factory=self.spec_factory,
),
},
detail_resolvers={
'app': TuiActionSlotResolver(
slot_templates={
'primary': self.static.create_app_run_template(),
'global': self.static.create_app_run_wizard_template(),
'utility': self.static.create_completion_install_template(),
},
spec_factory=self.spec_factory,
),
'database': TuiActionSlotResolver(
slot_templates={
'global': self.static.create_db_upgrade_wizard_template(),
'utility': self.static.create_db_init_dry_run_template(),
},
spec_factory=self.spec_factory,
),
'ops': TuiActionSlotResolver(
slot_templates={
'primary': self.static.create_ops_ping_db_template(),
'secondary': self.static.create_ops_ping_redis_template(),
'global': self.static.create_prod_check_wizard_template(),
},
spec_factory=self.spec_factory,
),
'crypto': TuiActionSlotResolver(
slot_templates={
'primary': self.static.create_crypto_keygen_template(),
'global': self.static.create_crypto_rotate_dry_run_template(),
},
spec_factory=self.spec_factory,
),
},
)
@@ -0,0 +1,31 @@
from cli.tui.actions.assembly import TuiActionRegistryBuilder
from cli.tui.actions.builders import TuiActionSpecFactory, TuiActionTemplateSupport
from cli.tui.actions.execution import TuiActionExecutionService
from cli.tui.actions.factories.cache import CacheActionTemplateFactory
from cli.tui.actions.factories.gen import GenActionTemplateFactory
from cli.tui.actions.factories.jobs import JobActionTemplateFactory
from cli.tui.actions.factories.static import StaticActionTemplateFactory
from cli.tui.actions.presentation import TuiActionPresentationService
from cli.tui.capabilities import TUI_CAPABILITY_REGISTRY
_ACTION_SPEC_FACTORY = TuiActionSpecFactory()
TUI_ACTION_EXECUTION_SERVICE = TuiActionExecutionService()
_ACTION_TEMPLATE_SUPPORT = TuiActionTemplateSupport(spec_factory=_ACTION_SPEC_FACTORY)
_JOB_ACTION_TEMPLATE_FACTORY = JobActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT)
_CACHE_ACTION_TEMPLATE_FACTORY = CacheActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT)
_GEN_ACTION_TEMPLATE_FACTORY = GenActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT)
_STATIC_ACTION_TEMPLATE_FACTORY = StaticActionTemplateFactory(support=_ACTION_TEMPLATE_SUPPORT)
_ACTION_REGISTRY_BUILDER = TuiActionRegistryBuilder(
jobs=_JOB_ACTION_TEMPLATE_FACTORY,
cache=_CACHE_ACTION_TEMPLATE_FACTORY,
gen=_GEN_ACTION_TEMPLATE_FACTORY,
static=_STATIC_ACTION_TEMPLATE_FACTORY,
spec_factory=_ACTION_SPEC_FACTORY,
)
TUI_ACTION_REGISTRY = _ACTION_REGISTRY_BUILDER.build()
TUI_ACTION_PRESENTATION_SERVICE = TuiActionPresentationService(
capability_registry=TUI_CAPABILITY_REGISTRY,
action_registry=TUI_ACTION_REGISTRY,
)
@@ -0,0 +1,329 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from cli.tui.actions.models import (
ActionCommandBuilder,
ActionExecutionMode,
ActionSummaryBuilder,
ActionTextBuilder,
TuiActionSpec,
)
from cli.tui.copy import TUI_COPY
from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER
if TYPE_CHECKING:
from cli.tui.adapters.models import BrowserRecordSnapshot
@dataclass(frozen=True)
class TuiActionSpecFactory:
"""
TUI 动作规格构建器。
该对象负责统一构建动作预览文本与 `TuiActionSpec`,
避免各动作函数继续手工拼接重复的预览和规格对象。
"""
@staticmethod
def parse_record_suffix(record: BrowserRecordSnapshot, prefix: str) -> str:
"""
从记录键中提取指定前缀后的标识。
:param record: 浏览记录
:param prefix: 键名前缀
:return: 后缀标识
"""
raw_key = str(record.key).strip()
token_prefix = f'{prefix}:'
if not raw_key.startswith(token_prefix):
return ''
return raw_key[len(token_prefix) :].strip()
def build_command_preview_lines(
self,
*,
env: str,
summary_lines: list[str],
command_args: tuple[str, ...],
append_yes: bool = True,
consequence_text: str = TUI_COPY.build_action_consequence_text('preview'),
) -> list[str]:
"""
构建统一的动作预览文本。
:param env: 当前运行环境
:param summary_lines: 业务摘要文本
:param command_args: 对应 CLI 命令参数
:param consequence_text: 结果影响说明
:return: 预览文本
"""
nested_cli_arguments = [
*command_args,
f'--env={env}',
'--output=json',
]
if append_yes:
nested_cli_arguments.append('--yes')
command = SHELL_TEXT_FORMATTER.format_shell_command(
NESTED_CLI_SUPPORT.build_nested_cli_command(*nested_cli_arguments)
)
return [
TUI_COPY.build_action_preview_title('summary'),
*summary_lines,
'',
TUI_COPY.build_action_preview_title('env'),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('target_env'), env),
'',
TUI_COPY.build_action_preview_title('command'),
command,
'',
TUI_COPY.build_action_preview_title('consequence'),
consequence_text,
]
def build_external_command_preview_lines(
self,
*,
env: str,
summary_lines: list[str],
command_args: tuple[str, ...],
append_yes: bool = True,
consequence_text: str = TUI_COPY.build_action_consequence_text('wizard'),
) -> list[str]:
"""
构建外部交互命令的预览文本。
:param env: 当前运行环境
:param summary_lines: 业务摘要文本
:param command_args: 对应 CLI 命令参数
:param consequence_text: 结果影响说明
:return: 预览文本
"""
return self.build_command_preview_lines(
env=env,
summary_lines=summary_lines,
command_args=command_args,
append_yes=append_yes,
consequence_text=consequence_text,
)
def build_nested_json_action(
self,
*,
action_id: str,
label: str,
command_args: tuple[str, ...],
env: str,
summary_lines: list[str],
preview_title: str | None = None,
consequence_text: str = TUI_COPY.build_action_consequence_text('preview'),
append_yes: bool = True,
refresh_view: bool = True,
) -> TuiActionSpec:
"""
构建标准 nested JSON 动作。
:param action_id: 动作唯一标识
:param label: 动作显示名称
:param command_args: 对应 CLI 命令参数
:param env: 当前运行环境
:param summary_lines: 业务摘要文本
:param preview_title: 预览标题
:param consequence_text: 结果影响说明
:param refresh_view: 执行后是否刷新视图
:return: 动作定义
"""
return TuiActionSpec(
action_id=action_id,
label=label,
command_args=command_args,
preview_title=preview_title or label,
preview_lines=self.build_command_preview_lines(
env=env,
summary_lines=summary_lines,
command_args=command_args,
append_yes=append_yes,
consequence_text=consequence_text,
),
append_yes=append_yes,
refresh_view=refresh_view,
)
def build_external_action(
self,
*,
action_id: str,
label: str,
command_args: tuple[str, ...],
env: str,
summary_lines: list[str],
preview_title: str | None = None,
consequence_text: str = TUI_COPY.build_action_consequence_text('wizard'),
append_yes: bool = True,
refresh_view: bool = True,
) -> TuiActionSpec:
"""
构建标准外部交互动作。
:param action_id: 动作唯一标识
:param label: 动作显示名称
:param command_args: 对应 CLI 命令参数
:param env: 当前运行环境
:param summary_lines: 业务摘要文本
:param preview_title: 预览标题
:param consequence_text: 结果影响说明
:param refresh_view: 执行后是否刷新视图
:return: 动作定义
"""
return TuiActionSpec(
action_id=action_id,
label=label,
command_args=command_args,
preview_title=preview_title or label,
preview_lines=self.build_external_command_preview_lines(
env=env,
summary_lines=summary_lines,
command_args=command_args,
append_yes=append_yes,
consequence_text=consequence_text,
),
execution_mode='external',
append_yes=append_yes,
refresh_view=refresh_view,
)
@dataclass(frozen=True)
class TuiActionTemplate:
"""
TUI 动作模板。
该模板将命令参数构建、摘要构建与执行模式收敛为统一对象,
使动作注册层可以直接声明“某个槽位对应哪类动作”。
:param action_id: 动作唯一标识
:param label: 动作显示名称
:param command_builder: 命令参数构建器
:param summary_builder: 预览摘要构建器
:param action_id_builder: 动态动作标识构建器
:param label_builder: 动态动作标题构建器
:param preview_title: 预览标题
:param consequence_text: 结果影响说明
:param execution_mode: 执行模式
:param append_yes: 是否在命令执行与预览中自动追加 `--yes`
:param refresh_view: 执行后是否刷新视图
:param preview_env_override: 预览中展示的环境名覆盖值
"""
action_id: str
label: str
command_builder: ActionCommandBuilder
summary_builder: ActionSummaryBuilder
action_id_builder: ActionTextBuilder | None = None
label_builder: ActionTextBuilder | None = None
preview_title: str | None = None
consequence_text: str = TUI_COPY.build_action_consequence_text('preview')
execution_mode: ActionExecutionMode = 'nested_json'
append_yes: bool = True
refresh_view: bool = True
preview_env_override: str | None = None
def build(
self,
*,
record: BrowserRecordSnapshot | None,
env: str,
spec_factory: TuiActionSpecFactory,
) -> TuiActionSpec | None:
"""
根据当前页面上下文构建动作定义。
:param record: 当前选中记录
:param env: 当前运行环境
:param spec_factory: 动作规格构建器
:return: 动作定义
"""
command_args = self.command_builder(record, env)
if command_args is None:
return None
preview_env = self.preview_env_override or env
summary_lines = self.summary_builder(record, env)
action_id = self.action_id_builder(record, env) if self.action_id_builder is not None else self.action_id
label = self.label_builder(record, env) if self.label_builder is not None else self.label
if self.execution_mode == 'external':
return spec_factory.build_external_action(
action_id=action_id,
label=label,
command_args=command_args,
env=preview_env,
summary_lines=summary_lines,
preview_title=self.preview_title,
consequence_text=self.consequence_text,
append_yes=self.append_yes,
refresh_view=self.refresh_view,
)
return spec_factory.build_nested_json_action(
action_id=action_id,
label=label,
command_args=command_args,
env=preview_env,
summary_lines=summary_lines,
preview_title=self.preview_title,
consequence_text=self.consequence_text,
append_yes=self.append_yes,
refresh_view=self.refresh_view,
)
@dataclass(frozen=True)
class TuiActionTemplateSupport:
"""
TUI 动作模板共享构建支持。
该对象集中封装多个动作模板都会复用的记录解析与摘要拼装逻辑,
避免这些基础能力继续散落在模块级函数中。
:param spec_factory: 动作规格构建器
"""
spec_factory: TuiActionSpecFactory
def build_scope_purpose_summary(self, scope: str, purpose: str) -> list[str]:
"""
构建通用的作用范围与目的摘要。
:param scope: 作用范围说明
:param purpose: 目的说明
:return: 预览摘要
"""
return [
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('scope'), scope),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('purpose'), purpose),
]
def extract_job_id(self, record: BrowserRecordSnapshot | None) -> str | None:
"""
从任务记录中提取任务 ID。
:param record: 当前记录
:return: 任务 ID
"""
if record is None:
return None
job_id = self.spec_factory.parse_record_suffix(record, 'job')
return job_id if job_id.isdigit() else None
@staticmethod
def require_record_title(record: BrowserRecordSnapshot | None) -> str | None:
"""
提取记录标题作为命令目标。
:param record: 当前记录
:return: 标题文本
"""
if record is None:
return None
title = str(record.title).strip()
return title or None
@@ -0,0 +1,121 @@
from dataclasses import dataclass
from cli.tui.actions.models import TuiActionResult, TuiActionSpec
from cli.tui.copy import TUI_COPY
from cli.utils import NESTED_CLI_SUPPORT, SHELL_TEXT_FORMATTER
@dataclass(frozen=True)
class TuiActionExecutionService:
"""
TUI 动作执行服务。
该对象负责统一封装 nested JSON 动作、外部交互动作
与结果文本行的收口逻辑。
"""
def execute(self, spec: TuiActionSpec, env: str) -> TuiActionResult:
"""
执行指定 TUI 动作。
:param spec: 动作定义
:param env: 当前运行环境
:return: 动作执行结果
"""
command_arguments = [
*spec.command_args,
f'--env={env}',
'--output=json',
]
if spec.append_yes:
command_arguments.append('--yes')
payload = NESTED_CLI_SUPPORT.run(*command_arguments, parse_json=True).payload
return TuiActionResult(spec=spec, payload=payload)
def execute_external(self, spec: TuiActionSpec) -> TuiActionResult:
"""
在当前终端中执行交互式 TUI 动作。
:param spec: 动作定义
:return: 动作执行结果
"""
completed = NESTED_CLI_SUPPORT.run_live(*spec.command_args)
if completed.returncode == 0:
message = '外部交互命令已执行完成'
else:
message = f'外部交互命令执行失败,退出码 {completed.returncode}'
return TuiActionResult(
spec=spec,
external_exit_code=completed.returncode,
external_message=message,
)
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')),
)
)
if result.external_exit_code is not None:
lines.append(
TUI_COPY.build_action_result_message_line(
TUI_COPY.build_action_result_field_label('exit_code'),
str(result.external_exit_code),
)
)
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
@@ -0,0 +1,83 @@
from dataclasses import dataclass
from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport
from cli.tui.adapters.models import BrowserRecordSnapshot
from cli.tui.copy import TUI_COPY
@dataclass(frozen=True)
class CacheActionTemplateFactory:
"""
缓存页动作模板工厂。
该对象负责生成缓存浏览页相关动作模板,统一缓存清理向导和预热动作
的命令与预览摘要构建。
:param support: 动作模板共享构建支持
"""
support: TuiActionTemplateSupport
def build_clear_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建缓存清理向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
cache_name = record.title if record is not None else ''
return (
'wizard',
'cache-clear',
'--output=text',
f'--default-env={env}',
'--default-mode=cache-name',
f'--default-cache-name={cache_name}',
'--default-dry-run',
)
def build_clear_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建缓存清理向导摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del record, env
return self.support.build_scope_purpose_summary(
'当前环境缓存清理流程',
'进入向导后确认缓存名、键前缀和 dry-run 范围',
)
def create_warmup_template(self) -> TuiActionTemplate:
"""
创建缓存预热动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='cache-warmup',
label=TUI_COPY.build_action_label('cache_warmup'),
command_builder=lambda record, env: ('cache', 'warmup'),
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('cache_warmup'),
TUI_COPY.build_action_purpose_label('cache_warmup'),
),
)
def create_clear_wizard_template(self) -> TuiActionTemplate:
"""
创建缓存清理向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-cache-clear',
label=TUI_COPY.build_action_label('cache_clear_wizard'),
command_builder=self.build_clear_command,
summary_builder=self.build_clear_summary,
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
)
@@ -0,0 +1,254 @@
from dataclasses import dataclass
from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport
from cli.tui.adapters.models import BrowserRecordSnapshot
from cli.tui.copy import TUI_COPY
@dataclass(frozen=True)
class GenActionTemplateFactory:
"""
代码生成页动作模板工厂。
该对象负责生成代码生成浏览页相关动作模板,统一记录标题提取、
导出导入向导和表结构同步动作的构建逻辑。
:param support: 动作模板共享构建支持
"""
support: TuiActionTemplateSupport
def build_export_wizard_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建代码生成导出向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
table_name = self.support.require_record_title(record)
if table_name is None:
return None
return (
'wizard',
'gen-export',
'--output=text',
f'--default-env={env}',
f'--default-table-names={table_name}',
'--default-mode=zip',
'--default-dry-run',
)
def build_import_wizard_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建代码生成导入向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
table_name = self.support.require_record_title(record)
if table_name is None:
return None
return (
'wizard',
'gen-import',
'--output=text',
f'--default-env={env}',
f'--default-table-names={table_name}',
'--default-dry-run',
)
def build_export_dry_run_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建代码生成导出 dry-run 命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del env
table_name = self.support.require_record_title(record)
if table_name is None:
return None
return ('gen', 'export', table_name, '--dry-run', '--mode=zip')
def build_sync_db_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建代码生成表结构同步命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del env
table_name = self.support.require_record_title(record)
if table_name is None:
return None
return ('gen', 'sync-db', table_name)
@staticmethod
def build_export_wizard_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建代码生成导出向导摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
return [
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('scope'), f'当前业务表 {record.title}'
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('purpose'),
'进入向导后选择业务表、输出目录和覆盖策略',
),
]
@staticmethod
def build_import_wizard_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建代码生成导入向导摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
return [
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('scope'), f'当前物理表 {record.title}'
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('purpose'),
'进入向导后确认物理表列表,并先执行 dry-run 导入预演',
),
]
@staticmethod
def build_export_dry_run_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建代码生成导出预演摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
return [
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('scope'),
TUI_COPY.build_action_scope_label('gen_export_dry_run'),
),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('purpose'),
TUI_COPY.build_action_purpose_label('gen_export_dry_run'),
),
]
@staticmethod
def build_sync_db_summary(record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建代码生成表结构同步摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
return [
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('scope'),
TUI_COPY.build_action_scope_label('gen_sync_db'),
),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('purpose'),
TUI_COPY.build_action_purpose_label('gen_sync_db'),
),
]
def create_export_wizard_template(self) -> TuiActionTemplate:
"""
创建代码生成导出向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-gen-export',
label=TUI_COPY.build_action_label('gen_export_wizard'),
command_builder=self.build_export_wizard_command,
summary_builder=self.build_export_wizard_summary,
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
refresh_view=False,
)
def create_import_wizard_template(self) -> TuiActionTemplate:
"""
创建代码生成导入向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-gen-import',
label=TUI_COPY.build_action_label('gen_import_wizard'),
command_builder=self.build_import_wizard_command,
summary_builder=self.build_import_wizard_summary,
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
refresh_view=False,
)
def create_export_dry_run_template(self) -> TuiActionTemplate:
"""
创建代码生成导出预演动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='gen-export-dry-run',
label=TUI_COPY.build_action_label('gen_export_dry_run'),
command_builder=self.build_export_dry_run_command,
summary_builder=self.build_export_dry_run_summary,
)
def create_sync_db_template(self) -> TuiActionTemplate:
"""
创建代码生成表结构同步动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='gen-sync-db',
label=TUI_COPY.build_action_label('gen_sync_db'),
command_builder=self.build_sync_db_command,
summary_builder=self.build_sync_db_summary,
)
@@ -0,0 +1,171 @@
from dataclasses import dataclass
from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport
from cli.tui.adapters.models import BrowserRecordSnapshot
from cli.tui.copy import TUI_COPY
@dataclass(frozen=True)
class JobActionTemplateFactory:
"""
任务页动作模板工厂。
该对象负责生成任务浏览页相关动作模板,收口任务 ID 解析、
暂停/恢复切换和摘要拼装逻辑。
:param support: 动作模板共享构建支持
"""
support: TuiActionTemplateSupport
def build_run_once_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建任务执行一次命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del env
job_id = self.support.extract_job_id(record)
if job_id is None:
return None
return ('job', 'run-once', job_id)
def build_run_once_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建任务执行一次动作摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
job_id = self.support.extract_job_id(record) or ''
return [
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job_id'), job_id),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
]
def build_toggle_command(self, record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...] | None:
"""
构建任务暂停或恢复命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del env
if record is None:
return None
job_id = self.support.extract_job_id(record)
if job_id is None:
return None
if record.status == 'warn':
return ('job', 'resume', job_id)
return ('job', 'pause', job_id)
def build_toggle_summary(self, record: BrowserRecordSnapshot | None, env: str) -> list[str]:
"""
构建任务暂停或恢复动作摘要。
:param record: 当前记录
:param env: 当前运行环境
:return: 预览摘要
"""
del env
if record is None:
return []
job_id = self.support.extract_job_id(record) or ''
label = (
TUI_COPY.build_action_label('job_resume')
if record.status == 'warn'
else TUI_COPY.build_action_label('job_pause')
)
return [
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job'), record.title),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('job_id'), job_id),
TUI_COPY.build_labeled_value_line(
TUI_COPY.build_action_preview_field_label('current_status'),
record.summary,
),
TUI_COPY.build_labeled_value_line(TUI_COPY.build_action_preview_field_label('target_action'), label),
]
@staticmethod
def build_toggle_action_id(record: BrowserRecordSnapshot | None, env: str) -> str:
"""
构建任务暂停或恢复动作标识。
:param record: 当前记录
:param env: 当前运行环境
:return: 动作标识
"""
del env
if record is not None and record.status == 'warn':
return 'job-resume'
return 'job-pause'
@staticmethod
def build_toggle_label(record: BrowserRecordSnapshot | None, env: str) -> str:
"""
构建任务暂停或恢复动作标题。
:param record: 当前记录
:param env: 当前运行环境
:return: 动作标题
"""
del env
if record is not None and record.status == 'warn':
return TUI_COPY.build_action_label('job_resume')
return TUI_COPY.build_action_label('job_pause')
def create_run_once_template(self) -> TuiActionTemplate:
"""
创建任务执行一次动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='job-run-once',
label=TUI_COPY.build_action_label('job_run_once'),
command_builder=self.build_run_once_command,
summary_builder=self.build_run_once_summary,
)
def create_toggle_template(self) -> TuiActionTemplate:
"""
创建任务暂停或恢复动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='job-toggle',
label=TUI_COPY.build_action_label('job_pause'),
command_builder=self.build_toggle_command,
summary_builder=self.build_toggle_summary,
action_id_builder=self.build_toggle_action_id,
label_builder=self.build_toggle_label,
)
def create_sync_template(self) -> TuiActionTemplate:
"""
创建任务同步动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='job-sync',
label=TUI_COPY.build_action_label('job_sync'),
command_builder=lambda record, env: ('job', 'sync'),
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('job_sync'),
TUI_COPY.build_action_purpose_label('job_sync'),
),
)
@@ -0,0 +1,385 @@
from dataclasses import dataclass
from cli.tui.actions.builders import TuiActionTemplate, TuiActionTemplateSupport
from cli.tui.adapters.models import BrowserRecordSnapshot
from cli.tui.copy import TUI_COPY
@dataclass(frozen=True)
class StaticActionTemplateFactory:
"""
静态页面动作模板工厂。
该对象负责生成不依赖当前记录内容的页面动作模板,统一详情页和
部分浏览页的固定动作定义。
:param support: 动作模板共享构建支持
"""
support: TuiActionTemplateSupport
@staticmethod
def build_job_sync_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建任务同步命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('job', 'sync')
@staticmethod
def build_config_sync_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建配置同步命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('config', 'sync-cache')
@staticmethod
def build_cache_warmup_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建缓存预热命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('cache', 'warmup')
@staticmethod
def build_db_upgrade_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建数据库升级向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record
return (
'wizard',
'db-upgrade',
'--output=text',
f'--default-env={env}',
'--default-revision=head',
'--default-dry-run',
)
@staticmethod
def build_db_init_dry_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建数据库初始化预演命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('db', 'init', '--dry-run')
@staticmethod
def build_app_run_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建应用启动向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('wizard', 'app-run')
@staticmethod
def build_completion_install_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建补全安装命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('completion', 'install', '--activate')
@staticmethod
def build_prod_check_wizard_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建生产巡检向导命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record
return (
'wizard',
'prod-check',
'--output=text',
f'--default-env={env}',
'--default-include-config',
)
@staticmethod
def build_crypto_rotate_dry_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建加密轮换预演命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('crypto', 'rotate', '--dry-run')
@staticmethod
def build_app_run_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建应用直接启动命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record
return ('app', 'run', f'--env={env}')
@staticmethod
def build_crypto_keygen_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建加密密钥生成命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record
return ('crypto', 'keygen', f'--env={env}', '--output=text')
@staticmethod
def build_ops_ping_db_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建数据库探活命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('ops', 'ping-db')
@staticmethod
def build_ops_ping_redis_command(record: BrowserRecordSnapshot | None, env: str) -> tuple[str, ...]:
"""
构建 Redis 探活命令参数。
:param record: 当前记录
:param env: 当前运行环境
:return: 命令参数
"""
del record, env
return ('ops', 'ping-redis')
def create_config_sync_template(self) -> TuiActionTemplate:
"""
创建配置同步动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='config-sync-cache',
label=TUI_COPY.build_action_label('config_sync'),
command_builder=self.build_config_sync_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('config_sync'),
TUI_COPY.build_action_purpose_label('config_sync'),
),
)
def create_db_upgrade_wizard_template(self) -> TuiActionTemplate:
"""
创建数据库升级向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-db-upgrade',
label=TUI_COPY.build_action_label('db_upgrade_wizard'),
command_builder=self.build_db_upgrade_wizard_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境数据库升级流程',
'进入向导后确认目标 revision、环境和 dry-run 预演',
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
)
def create_db_init_dry_run_template(self) -> TuiActionTemplate:
"""
创建数据库初始化预演动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='db-init-dry-run',
label=TUI_COPY.build_action_label('db_init_dry_run'),
command_builder=self.build_db_init_dry_run_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('db_init_dry_run'),
TUI_COPY.build_action_purpose_label('db_init_dry_run'),
),
append_yes=False,
)
def create_app_run_wizard_template(self) -> TuiActionTemplate:
"""
创建应用启动向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-app-run',
label=TUI_COPY.build_action_label('app_run_wizard'),
command_builder=self.build_app_run_wizard_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境应用启动流程',
'进入向导后确认环境,并决定是否先执行启动前检查',
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
refresh_view=False,
)
def create_completion_install_template(self) -> TuiActionTemplate:
"""
创建补全安装动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='completion-install',
label=TUI_COPY.build_action_label('completion_install'),
command_builder=self.build_completion_install_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('completion_install'),
TUI_COPY.build_action_purpose_label('completion_install'),
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('external'),
refresh_view=False,
preview_env_override='-',
)
def create_prod_check_wizard_template(self) -> TuiActionTemplate:
"""
创建生产巡检向导动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='wizard-prod-check',
label=TUI_COPY.build_action_label('prod_check_wizard'),
command_builder=self.build_prod_check_wizard_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境生产巡检流程',
'进入向导后统一检查数据库、缓存和运行配置',
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
)
def create_crypto_rotate_dry_run_template(self) -> TuiActionTemplate:
"""
创建加密轮换预演动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='crypto-rotate-dry-run',
label=TUI_COPY.build_action_label('crypto_rotate_dry_run'),
command_builder=self.build_crypto_rotate_dry_run_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境传输加密轮换预演',
'仅生成轮换辅助结果,不直接写入新密钥配置',
),
append_yes=False,
)
def create_app_run_template(self) -> TuiActionTemplate:
"""
创建应用直接启动动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='app-run',
label=TUI_COPY.build_action_label('app_run'),
command_builder=self.build_app_run_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境应用直接启动流程',
'确认后会在当前终端直接启动当前 FastAPI 应用,并持续占用终端会话',
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
refresh_view=False,
)
def create_crypto_keygen_template(self) -> TuiActionTemplate:
"""
创建密钥生成动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='crypto-keygen',
label=TUI_COPY.build_action_label('crypto_keygen'),
command_builder=self.build_crypto_keygen_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
'当前环境新密钥生成流程',
'在终端中直接输出新密钥材料和环境变量补丁建议',
),
execution_mode='external',
consequence_text=TUI_COPY.build_action_consequence_text('wizard'),
refresh_view=False,
)
def create_ops_ping_db_template(self) -> TuiActionTemplate:
"""
创建数据库探活动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='ops-ping-db',
label=TUI_COPY.build_action_label('ops_ping_db'),
command_builder=self.build_ops_ping_db_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('ops_ping_db'),
TUI_COPY.build_action_purpose_label('ops_ping_db'),
),
append_yes=False,
)
def create_ops_ping_redis_template(self) -> TuiActionTemplate:
"""
创建 Redis 探活动作模板。
:return: 动作模板
"""
return TuiActionTemplate(
action_id='ops-ping-redis',
label=TUI_COPY.build_action_label('ops_ping_redis'),
command_builder=self.build_ops_ping_redis_command,
summary_builder=lambda record, env: self.support.build_scope_purpose_summary(
TUI_COPY.build_action_scope_label('ops_ping_redis'),
TUI_COPY.build_action_purpose_label('ops_ping_redis'),
),
append_yes=False,
)
@@ -0,0 +1,72 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Literal
from cli.tui.adapters.models import TUI_ADAPTER_MODEL_RENDERER, BrowserRecordSnapshot
ActionSlot = Literal['primary', 'secondary', 'global', 'utility']
ActionExecutionMode = Literal['nested_json', 'external']
ActionCommandBuilder = Callable[[BrowserRecordSnapshot | None, str], tuple[str, ...] | None]
ActionSummaryBuilder = Callable[[BrowserRecordSnapshot | None, str], list[str]]
ActionTextBuilder = Callable[[BrowserRecordSnapshot | None, str], str]
@dataclass(frozen=True)
class TuiActionSpec:
"""
TUI 低风险动作定义。
:param action_id: 动作唯一标识
:param label: 动作显示名称
:param command_args: 对应 CLI 命令参数
:param preview_title: 预览弹窗标题
:param preview_lines: 预览摘要文本
:param append_yes: 执行 nested JSON 动作时是否自动追加 `--yes`
:param refresh_view: 执行完成后是否刷新当前页面
"""
action_id: str
label: str
command_args: tuple[str, ...]
preview_title: str
preview_lines: list[str]
execution_mode: ActionExecutionMode = 'nested_json'
append_yes: bool = True
refresh_view: bool = True
@dataclass(frozen=True)
class TuiActionResult:
"""
TUI 动作执行结果。
:param spec: 动作定义
:param payload: CLI JSON 负载
"""
spec: TuiActionSpec
payload: dict[str, object] | None = None
external_exit_code: int | None = None
external_message: str | None = None
@property
def ok(self) -> bool:
"""
判断动作是否执行成功。
:return: 是否成功
"""
if self.external_exit_code is not None:
return self.external_exit_code == 0
return bool(isinstance(self.payload, dict) and self.payload.get('ok', False))
@property
def message(self) -> str:
"""
获取动作结果摘要。
:return: 结果摘要
"""
if self.external_message is not None:
return self.external_message
return TUI_ADAPTER_MODEL_RENDERER.extract_payload_message(self.payload)
@@ -0,0 +1,110 @@
from dataclasses import dataclass
from cli.tui.actions.models import TuiActionResult
from cli.tui.actions.registry import TuiActionRegistry
from cli.tui.adapters.models import BrowserRecordSnapshot
from cli.tui.capabilities import TuiCapabilityRegistry
from cli.tui.copy import TUI_COPY
from cli.tui.keymaps import TUI_KEYMAP_REGISTRY
from cli.utils import SHELL_TEXT_FORMATTER
@dataclass(frozen=True)
class TuiActionPresentationService:
"""
TUI 动作展示服务。
该对象负责动作提示、动作列表和动作结果通知等展示层文本构建,
避免继续保留模块级桥接函数。
"""
capability_registry: TuiCapabilityRegistry
action_registry: TuiActionRegistry
def build_browser_action_hint(self, view_key: str) -> str:
"""
构建浏览页动作提示文本。
:param view_key: 当前页面视图标识
:return: 动作提示文本
"""
capability_labels = [
f'[{capability.key}] {capability.hint_label}'
for capability in self.capability_registry.get_browser_capabilities(view_key)
]
interaction_text = TUI_COPY.build_browser_action_hint_text(
view_key, TUI_KEYMAP_REGISTRY.browser_interaction_hint
)
return TUI_COPY.build_capability_hint_text(
capability_labels,
interaction_text,
fallback='{interaction_hint}',
)
def build_detail_action_hint(self, view_key: str) -> str:
"""
构建详情页动作提示文本。
:param view_key: 当前页面视图标识
:return: 动作提示文本
"""
capability_labels = [
f'[{capability.key}] {capability.hint_label}'
for capability in self.capability_registry.get_detail_capabilities(view_key)
]
interaction_text = TUI_COPY.build_detail_action_hint_text(
view_key, TUI_KEYMAP_REGISTRY.browser_interaction_hint
)
return TUI_COPY.build_capability_hint_text(
capability_labels,
interaction_text,
fallback='{interaction_hint}',
)
def build_browser_action_lines(
self,
*,
view_key: str,
record: BrowserRecordSnapshot | None,
env: str,
) -> list[str]:
"""
构建当前页面可执行动作列表。
:param view_key: 当前页面视图标识
:param record: 当前选中记录
:param env: 当前运行环境
:return: 动作文本行
"""
lines: list[str] = []
for capability in self.capability_registry.get_browser_capabilities(view_key):
slot = capability.slot # type: ignore[assignment]
action = self.action_registry.resolve_browser_action(
view_key=view_key,
slot=slot,
record=record,
env=env,
)
if action is None:
continue
lines.append(TUI_COPY.build_action_line(capability.key, action.label))
return lines or [TUI_COPY.build_action_empty_line()]
@staticmethod
def build_action_result_message(result: TuiActionResult) -> str:
"""
构建动作执行结果通知文本。
:param result: 动作执行结果
:return: 通知文本
"""
outcome = (
TUI_COPY.build_action_result_field_label('success')
if result.ok
else TUI_COPY.build_action_result_field_label('fail')
)
return TUI_COPY.build_action_result_toast(
result.spec.label,
outcome,
SHELL_TEXT_FORMATTER.truncate_text(result.message, 72),
)
@@ -0,0 +1,103 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cli.tui.actions.builders import TuiActionSpecFactory, TuiActionTemplate
from cli.tui.actions.models import ActionSlot, TuiActionSpec
from cli.tui.adapters.models import BrowserRecordSnapshot
@dataclass(frozen=True)
class TuiActionRegistry:
"""
TUI 动作注册表。
该注册表负责集中维护浏览页和详情页的动作解析器,从而将
原本散落的条件分发收敛为可声明的映射关系。
:param browser_resolvers: 浏览页动作槽位解析器映射
:param detail_resolvers: 详情页动作槽位解析器映射
"""
browser_resolvers: dict[str, TuiActionSlotResolver]
detail_resolvers: dict[str, TuiActionSlotResolver]
def resolve_browser_action(
self,
*,
view_key: str,
slot: ActionSlot,
record: BrowserRecordSnapshot | None,
env: str,
) -> TuiActionSpec | None:
"""
解析浏览页动作。
:param view_key: 当前页面视图标识
:param slot: 动作槽位
:param record: 当前选中记录
:param env: 当前运行环境
:return: 动作定义
"""
resolver = self.browser_resolvers.get(view_key)
if resolver is None:
return None
return resolver.resolve(slot=slot, record=record, env=env)
def resolve_detail_action(
self,
*,
view_key: str,
slot: ActionSlot,
env: str,
) -> TuiActionSpec | None:
"""
解析详情页动作。
:param view_key: 当前页面视图标识
:param slot: 动作槽位
:param env: 当前运行环境
:return: 动作定义
"""
resolver = self.detail_resolvers.get(view_key)
if resolver is None:
return None
return resolver.resolve(slot=slot, record=None, env=env)
@dataclass(frozen=True)
class TuiActionSlotResolver:
"""
TUI 动作槽位解析器。
该对象将一个页面支持的槽位动作集中定义为映射关系,避免继续在
`resolve_*_action` 中堆叠条件分支。
:param slot_templates: 槽位到动作模板的映射
:param spec_factory: 动作规格构建器
"""
slot_templates: dict[ActionSlot, TuiActionTemplate]
spec_factory: TuiActionSpecFactory
def resolve(
self,
*,
slot: ActionSlot,
record: BrowserRecordSnapshot | None,
env: str,
) -> TuiActionSpec | None:
"""
解析指定槽位的动作定义。
:param slot: 动作槽位
:param record: 当前选中记录
:param env: 当前运行环境
:return: 动作定义
"""
template = self.slot_templates.get(slot)
if template is None:
return None
return template.build(record=record, env=env, spec_factory=self.spec_factory)