mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-09-24 05:26:59 +00:00
* feat: 初始化插件系统 * refactor: 收口插件系统运行时重构 * perf: 优化插件系统类型提示 * fix&perf: 修复和优化插件系统 * fix: 修复gitignore规则误忽略插件文件的问题 * fix: 修复运行时插件根路径算错的问题 * fix: 加强插件发现和路由注册的防护措施 * revert: 回滚定时任务白名单 * fix: 移除未使用的应用路由注册探测 * revert: 恢复部分代码 * perf: 优化插件系统 * docs: 新增插件开发文档 * perf: 优化插件管理模块 * perf: 提升插件系统核心能力 * refactor: 重构生命周期 step runner * fix: 修复lint错误 * test: 清理测试用例 * test: 调整测试目录名称 * fix: 修复前后端目录硬编码的问题 * fix: 修复插件系统安全性缺口 * refactor: 重新设计插件生命周期 Migration 事务与回滚 * perf: 优化插件系统边界问题 * refactor: 重构当前插件系统的依赖体系设计 * perf: 优化代码 * perf: 优化代码 * fix: 修复代码合并问题 * fix: 修复bug * perf: 优化代码 * perf&fix: 优化代码和修复bug * docs: 优化文档格式 * feat: 适配Vue2版本 * docs: 更新README文档 * fix: 修复ruff lint错误 * chore: 更新后端依赖文件
1223 lines
38 KiB
Python
1223 lines
38 KiB
Python
import importlib
|
|
import json
|
|
import sys
|
|
from collections.abc import Callable
|
|
from typing import TYPE_CHECKING
|
|
|
|
import click
|
|
import typer
|
|
|
|
from cli.core import DEFAULT_CORE_SERVICES, CliContextFactory, CliExecutionService
|
|
from cli.exit_codes import ARGUMENT_ERROR, DEPENDENCY_ERROR, RUNTIME_ERROR, SUCCESS
|
|
from plugins.core.validation.dependency_policy import DependencyInstallPolicyConfig
|
|
|
|
from .exporter import PluginCommandFileAdapter
|
|
from .options import (
|
|
PluginCreateCommandOptions,
|
|
PluginDependencyAllowlistExampleCommandOptions,
|
|
PluginDependencyInstallCommandOptions,
|
|
PluginDependencyLockCommandOptions,
|
|
)
|
|
from .payload import PluginCommandPayloadAdapter
|
|
from .presenter import PluginCommandPresenter
|
|
|
|
if TYPE_CHECKING:
|
|
from cli.runtime.plugin.service import CliPluginRuntimeService
|
|
from plugins.core.runtime.service import PluginRuntimeService
|
|
|
|
PluginDependencyOutputCallback = Callable[[str, str], None]
|
|
|
|
|
|
class PluginCommandController:
|
|
"""
|
|
插件命令控制器。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
context_factory: CliContextFactory | None = None,
|
|
execution_service: CliExecutionService | None = None,
|
|
presenter: PluginCommandPresenter | None = None,
|
|
plugin_runtime: 'CliPluginRuntimeService | None' = None,
|
|
) -> None:
|
|
"""
|
|
初始化插件命令控制器。
|
|
|
|
:param context_factory: CLI 上下文工厂
|
|
:param execution_service: CLI 执行服务
|
|
:param presenter: 插件命令文本渲染器
|
|
:param plugin_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 PluginCommandPresenter()
|
|
self._plugin_runtime = plugin_runtime
|
|
|
|
@property
|
|
def plugin_runtime(self) -> 'CliPluginRuntimeService':
|
|
"""
|
|
延迟获取插件 CLI 运行时服务。
|
|
|
|
:return: 插件 CLI 运行时服务
|
|
"""
|
|
if self._plugin_runtime is None:
|
|
self._plugin_runtime = importlib.import_module('cli.runtime.plugin').PLUGIN_RUNTIME
|
|
return self._plugin_runtime
|
|
|
|
@property
|
|
def core_runtime(self) -> 'PluginRuntimeService':
|
|
"""
|
|
获取插件核心运行时服务。
|
|
|
|
:return: 插件核心运行时服务
|
|
"""
|
|
return self.plugin_runtime.core_runtime
|
|
|
|
def list_plugins(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.core_runtime.list_plugins_with_state())
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_list_text,
|
|
failure_exit_code=RUNTIME_ERROR,
|
|
)
|
|
|
|
def _complete_plugin_payload(
|
|
self,
|
|
ctx: object,
|
|
payload: dict[str, object],
|
|
*,
|
|
text_builder: object,
|
|
success_exit_code: int = SUCCESS,
|
|
failure_exit_code: int = DEPENDENCY_ERROR,
|
|
) -> None:
|
|
"""
|
|
按插件 payload ok 字段统一完成命令输出。
|
|
|
|
:param ctx: CLI上下文
|
|
:param payload: 插件操作负载
|
|
:param text_builder: 文本构造器
|
|
:param success_exit_code: 成功退出码
|
|
:param failure_exit_code: 失败退出码
|
|
:return: None
|
|
"""
|
|
payload = PluginCommandPayloadAdapter.adapt(payload)
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
payload,
|
|
text_builder=text_builder,
|
|
default_exit_code=self._resolve_plugin_exit_code(
|
|
payload,
|
|
success_exit_code=success_exit_code,
|
|
failure_exit_code=failure_exit_code,
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def _resolve_plugin_exit_code(
|
|
payload: dict[str, object],
|
|
*,
|
|
success_exit_code: int,
|
|
failure_exit_code: int,
|
|
) -> int:
|
|
"""
|
|
按插件 payload 形状解析 CLI 退出码。
|
|
|
|
:param payload: 插件操作负载
|
|
:param success_exit_code: 成功退出码
|
|
:param failure_exit_code: 业务失败退出码
|
|
:return: CLI 退出码
|
|
"""
|
|
if bool(payload.get('ok', False)):
|
|
return success_exit_code
|
|
if payload.get('error'):
|
|
return RUNTIME_ERROR
|
|
|
|
return failure_exit_code
|
|
|
|
def plugin_info(self, plugin_id: str, env: str, output: str) -> None:
|
|
"""
|
|
查看插件详情。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.get_plugin_info_with_state(plugin_id))
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_info_text,
|
|
failure_exit_code=RUNTIME_ERROR,
|
|
)
|
|
|
|
def check_plugin(self, plugin_id: str | None, env: str, output: str) -> None:
|
|
"""
|
|
检查插件依赖状态。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.core_runtime.check_plugin(plugin_id)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_check_text,
|
|
)
|
|
|
|
def check_plugin_dependencies(self, plugin_id: str, env: str, output: str) -> None:
|
|
"""
|
|
检查插件依赖。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.core_runtime.check_plugin_dependencies(plugin_id)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_text,
|
|
)
|
|
|
|
def precheck_plugin(self, operation: str, plugin_id: str, env: str, output: str) -> None:
|
|
"""
|
|
执行插件操作预检。
|
|
|
|
:param operation: 预检操作类型
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.precheck_plugin_operation(plugin_id, operation))
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_precheck_text,
|
|
)
|
|
|
|
def health_plugin(self, plugin_id: str, env: str, output: str) -> None:
|
|
"""
|
|
执行插件健康检查。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.health_plugin(plugin_id))
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_health_text,
|
|
)
|
|
|
|
def diagnose_plugin(self, plugin_id: str, env: str, output: str, *, output_file: str = '') -> None:
|
|
"""
|
|
生成插件诊断包。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param output_file: 诊断包 JSON 导出文件路径
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.diagnose_plugin(plugin_id))
|
|
if output_file.strip():
|
|
payload = PluginCommandFileAdapter.write_json_file(
|
|
payload,
|
|
output_file,
|
|
failure_message='插件诊断包导出失败',
|
|
)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_diagnose_text,
|
|
)
|
|
|
|
def generate_plugin_docs(self, plugin_id: str, env: str, output: str, *, output_file: str = '') -> None:
|
|
"""
|
|
生成插件 Markdown 文档片段。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param output_file: Markdown 文档导出文件路径
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.core_runtime.generate_plugin_docs(plugin_id)
|
|
if output_file.strip():
|
|
payload = PluginCommandFileAdapter.write_markdown_file(
|
|
payload,
|
|
output_file,
|
|
content_key='markdown',
|
|
failure_message='插件文档导出失败',
|
|
)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_docs_text,
|
|
)
|
|
|
|
def test_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
keyword: str = '',
|
|
maxfail: int = 0,
|
|
quiet: bool = False,
|
|
frontend_build: bool = False,
|
|
) -> None:
|
|
"""
|
|
执行插件测试。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param keyword: pytest 关键字过滤表达式
|
|
:param maxfail: 最大失败数
|
|
:param quiet: 是否启用简洁输出
|
|
:param frontend_build: 是否执行前端构建验收
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.plugin_runtime.test_plugin(
|
|
plugin_id,
|
|
keyword=keyword,
|
|
maxfail=maxfail,
|
|
quiet=quiet,
|
|
frontend_build=frontend_build,
|
|
)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_test_text,
|
|
)
|
|
|
|
def plan_plugins(self, operation: str, plugin_ids: list[str], env: str, output: str) -> None:
|
|
"""
|
|
生成插件批量操作拓扑计划。
|
|
|
|
:param operation: 计划操作类型
|
|
:param plugin_ids: 插件ID列表
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.core_runtime.plan_plugins(operation, plugin_ids or None)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_plan_text,
|
|
)
|
|
|
|
def batch_plugins(
|
|
self,
|
|
operation: str,
|
|
plugin_ids: list[str],
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
continue_on_error: bool,
|
|
) -> None:
|
|
"""
|
|
按拓扑顺序批量执行插件操作。
|
|
|
|
:param operation: 批量操作类型
|
|
:param plugin_ids: 插件ID列表
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param dry_run: 是否仅预演
|
|
:param continue_on_error: 失败后是否继续执行后续插件
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
dry_run,
|
|
command_name='plugin batch',
|
|
)
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.batch_plugins(
|
|
operation,
|
|
plugin_ids or None,
|
|
dry_run=dry_run,
|
|
continue_on_error=continue_on_error,
|
|
)
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_batch_text,
|
|
)
|
|
|
|
def install_plugin_dependencies(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
options: PluginDependencyInstallCommandOptions,
|
|
) -> None:
|
|
"""
|
|
安装插件依赖。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param options: 依赖安装命令选项
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_regular(
|
|
env,
|
|
output,
|
|
options.allow_prod,
|
|
options.yes,
|
|
options.dry_run,
|
|
)
|
|
policy_config = DependencyInstallPolicyConfig.from_environment(
|
|
env=env,
|
|
mode=options.policy_mode,
|
|
allow_prod=options.allow_prod,
|
|
allow_unlisted=options.allow_unlisted,
|
|
lockfile_path=options.lockfile or None,
|
|
offline_dir=options.offline_dir or None,
|
|
require_lockfile=options.require_lockfile,
|
|
)
|
|
core_runtime = self.core_runtime
|
|
output_callback = self._build_dependency_install_output_callback(ctx)
|
|
if self._should_interactive_confirm_dependency_install(ctx, options):
|
|
preview_payload = core_runtime.install_plugin_dependencies(
|
|
plugin_id,
|
|
dry_run=True,
|
|
policy_config=policy_config,
|
|
confirmed=True,
|
|
)
|
|
preview_payload['env'] = ctx.env
|
|
if self._dependency_install_policy_blocked(preview_payload):
|
|
payload = core_runtime.install_plugin_dependencies(
|
|
plugin_id,
|
|
dry_run=False,
|
|
policy_config=policy_config,
|
|
confirmed=True,
|
|
output_callback=output_callback,
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_install_text,
|
|
)
|
|
return
|
|
typer.echo(self.presenter.build_dependency_install_text(preview_payload))
|
|
if not self._confirm_dependency_install(ctx.env):
|
|
payload = self._build_dependency_install_cancel_payload(plugin_id, ctx.env, preview_payload)
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_install_text,
|
|
)
|
|
return
|
|
confirmed = True
|
|
else:
|
|
confirmed = options.yes
|
|
|
|
payload = core_runtime.install_plugin_dependencies(
|
|
plugin_id,
|
|
dry_run=options.dry_run,
|
|
policy_config=policy_config,
|
|
confirmed=confirmed,
|
|
output_callback=output_callback,
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_install_text,
|
|
)
|
|
|
|
@staticmethod
|
|
def _build_dependency_install_output_callback(ctx: object) -> PluginDependencyOutputCallback | None:
|
|
"""
|
|
为文本输出模式构建依赖安装实时输出回调。
|
|
|
|
:param ctx: CLI上下文
|
|
:return: 实时输出回调;非文本模式返回 None
|
|
"""
|
|
if getattr(ctx, 'output', 'text') != 'text':
|
|
return None
|
|
|
|
def output_callback(kind: str, text: str) -> None:
|
|
if text:
|
|
typer.echo(text, nl=False, err=kind == 'stderr')
|
|
|
|
return output_callback
|
|
|
|
def lock_plugin_dependencies(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
options: PluginDependencyLockCommandOptions,
|
|
) -> None:
|
|
"""
|
|
生成插件依赖锁文件模板。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param options: 依赖锁文件命令选项
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_regular(
|
|
env,
|
|
output,
|
|
False,
|
|
True,
|
|
options.dry_run,
|
|
)
|
|
payload = self.plugin_runtime.lock_plugin_dependencies(
|
|
plugin_id,
|
|
output_path=options.output_path,
|
|
offline_dir=options.offline_dir,
|
|
dry_run=options.dry_run,
|
|
overwrite=options.overwrite,
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_lock_text,
|
|
)
|
|
|
|
def generate_plugin_dependency_allowlist_example(
|
|
self,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
options: PluginDependencyAllowlistExampleCommandOptions,
|
|
) -> None:
|
|
"""
|
|
生成插件依赖允许列表示例。
|
|
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param options: 允许列表示例命令选项
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_regular(
|
|
env,
|
|
output,
|
|
False,
|
|
True,
|
|
options.dry_run,
|
|
)
|
|
payload = self.plugin_runtime.generate_plugin_dependency_allowlist_example(
|
|
output_path=options.output_path,
|
|
dry_run=options.dry_run,
|
|
overwrite=options.overwrite,
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_dependency_allowlist_example_text,
|
|
)
|
|
|
|
@staticmethod
|
|
def _should_interactive_confirm_dependency_install(
|
|
ctx: object,
|
|
options: PluginDependencyInstallCommandOptions,
|
|
) -> bool:
|
|
"""
|
|
判断插件依赖安装是否应进入 CLI 交互确认流程。
|
|
|
|
:param ctx: CLI上下文
|
|
:param options: 依赖安装命令选项
|
|
:return: 是否进入交互确认
|
|
"""
|
|
return (
|
|
not options.yes and not options.dry_run and getattr(ctx, 'output', 'text') == 'text' and sys.stdin.isatty()
|
|
)
|
|
|
|
@staticmethod
|
|
def _dependency_install_policy_blocked(payload: dict[str, object]) -> bool:
|
|
"""
|
|
判断依赖安装预览中的策略是否已阻断真实安装。
|
|
|
|
:param payload: 依赖安装预览负载
|
|
:return: 是否被策略阻断
|
|
"""
|
|
policy = payload.get('policy')
|
|
return isinstance(policy, dict) and policy.get('allowed') is False
|
|
|
|
@staticmethod
|
|
def _confirm_dependency_install(env: str) -> bool:
|
|
"""
|
|
询问用户是否执行插件依赖安装。
|
|
|
|
:param env: 当前运行环境
|
|
:return: 是否确认执行
|
|
"""
|
|
try:
|
|
return bool(typer.confirm(f'确认执行插件依赖安装吗? 当前环境:{env}', default=False))
|
|
except (click.Abort, EOFError, KeyboardInterrupt):
|
|
return False
|
|
|
|
@staticmethod
|
|
def _build_dependency_install_cancel_payload(
|
|
plugin_id: str,
|
|
env: str,
|
|
preview_payload: dict[str, object],
|
|
) -> dict[str, object]:
|
|
"""
|
|
构建用户取消插件依赖安装的负载。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前运行环境
|
|
:param preview_payload: 安装预览负载
|
|
:return: 取消安装负载
|
|
"""
|
|
return {
|
|
'ok': False,
|
|
'message': '已取消插件依赖安装',
|
|
'pluginId': plugin_id,
|
|
'env': env,
|
|
'dryRun': False,
|
|
'preview': preview_payload,
|
|
}
|
|
|
|
def create_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
options: PluginCreateCommandOptions,
|
|
) -> None:
|
|
"""
|
|
创建插件开发模板。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param options: 插件创建命令选项
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
if options.backend_only and options.frontend_only:
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
{
|
|
'ok': False,
|
|
'message': '--backend-only 和 --frontend-only 不能同时使用',
|
|
},
|
|
text_builder=self.presenter.build_create_text,
|
|
default_exit_code=ARGUMENT_ERROR,
|
|
)
|
|
return
|
|
|
|
payload = self.plugin_runtime.create_plugin(
|
|
plugin_id,
|
|
template=options.template,
|
|
backend=not options.frontend_only,
|
|
frontend=not options.backend_only,
|
|
migration=not options.no_migration,
|
|
seed=not options.no_seed,
|
|
job=not options.no_job,
|
|
config=not options.no_config,
|
|
test=not options.no_test,
|
|
frontend_version=options.frontend_version,
|
|
dry_run=options.dry_run,
|
|
)
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_create_text,
|
|
default_exit_code=SUCCESS,
|
|
)
|
|
|
|
def install_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
安装插件。
|
|
|
|
:param plugin_id: 插件ID
|
|
: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='plugin install',
|
|
)
|
|
payload = self.execution_service.run_async(self.core_runtime.install_plugin(plugin_id, dry_run=dry_run))
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_install_text,
|
|
)
|
|
|
|
def upgrade_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
升级插件。
|
|
|
|
:param plugin_id: 插件ID
|
|
: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='plugin upgrade',
|
|
)
|
|
payload = self.execution_service.run_async(self.core_runtime.upgrade_plugin(plugin_id, dry_run=dry_run))
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_upgrade_text,
|
|
)
|
|
|
|
def set_plugin_enabled(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
enabled: bool,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
更新插件启停状态。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param enabled: 是否启用
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param dry_run: 是否仅预演
|
|
:return: None
|
|
"""
|
|
command_name = 'plugin enable' if enabled else 'plugin disable'
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
dry_run,
|
|
command_name=command_name,
|
|
)
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.set_plugin_enabled(plugin_id, enabled=enabled, dry_run=dry_run)
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_enabled_text,
|
|
)
|
|
|
|
def uninstall_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
安全卸载插件。
|
|
|
|
:param plugin_id: 插件ID
|
|
: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='plugin uninstall',
|
|
)
|
|
payload = self.execution_service.run_async(self.core_runtime.uninstall_plugin(plugin_id, dry_run=dry_run))
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_enabled_text,
|
|
)
|
|
|
|
def purge_plugin(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
物理清理插件平台元数据。
|
|
|
|
:param plugin_id: 插件ID
|
|
: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='plugin purge',
|
|
)
|
|
payload = self.execution_service.run_async(self.core_runtime.purge_plugin(plugin_id, dry_run=dry_run))
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_purge_text,
|
|
)
|
|
|
|
def list_plugin_migrations(self, plugin_id: str, status: str | None, env: str, output: str) -> None:
|
|
"""
|
|
查看插件 migration 历史。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param status: 执行状态
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.list_plugin_migrations(plugin_id, status))
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_migration_list_text,
|
|
)
|
|
|
|
def mark_plugin_migration_success(
|
|
self,
|
|
plugin_id: str,
|
|
migration_path: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
note: str,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
人工标记插件 migration 为成功。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param migration_path: migration 相对路径
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param note: 人工恢复备注
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
False,
|
|
command_name='plugin mark-success',
|
|
)
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.mark_plugin_migration_success(plugin_id, migration_path, note=note or None)
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_migration_mark_text,
|
|
)
|
|
|
|
def mark_plugin_migration_failed(
|
|
self,
|
|
plugin_id: str,
|
|
migration_path: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
note: str,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
人工标记插件 migration 为失败。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param migration_path: migration 相对路径
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param note: 人工恢复备注
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
False,
|
|
command_name='plugin mark-failed',
|
|
)
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.mark_plugin_migration_failed(plugin_id, migration_path, note=note or None)
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_migration_mark_text,
|
|
)
|
|
|
|
def plugin_config(
|
|
self,
|
|
plugin_id: str,
|
|
action: str,
|
|
pairs: list[str],
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
reveal_secret: bool = False,
|
|
output_file: str = '',
|
|
input_file: str = '',
|
|
) -> None:
|
|
"""
|
|
查看或设置插件配置。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param action: 操作类型
|
|
:param pairs: 配置键值列表
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param reveal_secret: 是否导出敏感配置明文
|
|
:param output_file: 配置导出 JSON 文件路径
|
|
:param input_file: 配置导入 JSON 文件路径
|
|
:return: None
|
|
"""
|
|
if action == 'get':
|
|
self._get_plugin_config(plugin_id, env, output)
|
|
return
|
|
|
|
if action == 'export':
|
|
self._export_plugin_config(
|
|
plugin_id,
|
|
env,
|
|
output,
|
|
allow_prod=allow_prod,
|
|
yes=yes,
|
|
reveal_secret=reveal_secret,
|
|
output_file=output_file,
|
|
)
|
|
return
|
|
|
|
if action == 'import':
|
|
self._import_plugin_config(
|
|
plugin_id,
|
|
env,
|
|
output,
|
|
allow_prod=allow_prod,
|
|
yes=yes,
|
|
input_file=input_file,
|
|
)
|
|
return
|
|
|
|
if action != 'set':
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
{
|
|
'ok': False,
|
|
'message': '插件配置操作只支持 get、set、export 或 import',
|
|
'pluginId': plugin_id,
|
|
},
|
|
text_builder=self.presenter.build_config_text,
|
|
default_exit_code=ARGUMENT_ERROR,
|
|
)
|
|
return
|
|
|
|
self._set_plugin_config(
|
|
plugin_id,
|
|
pairs,
|
|
env,
|
|
output,
|
|
allow_prod=allow_prod,
|
|
yes=yes,
|
|
)
|
|
|
|
def _get_plugin_config(self, plugin_id: str, env: str, output: str) -> None:
|
|
"""
|
|
读取插件配置。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_readonly(env, output)
|
|
payload = self.execution_service.run_async(self.core_runtime.get_plugin_config(plugin_id))
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_config_text,
|
|
)
|
|
|
|
def _export_plugin_config(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
reveal_secret: bool,
|
|
output_file: str,
|
|
) -> None:
|
|
"""
|
|
导出插件配置。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param reveal_secret: 是否导出敏感配置明文
|
|
:param output_file: 配置导出 JSON 文件路径
|
|
:return: None
|
|
"""
|
|
ctx = (
|
|
self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
dry_run=True,
|
|
command_name='plugin config export',
|
|
)
|
|
if reveal_secret
|
|
else self.context_factory.build_readonly(env, output)
|
|
)
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.export_plugin_config(plugin_id, reveal_secret=reveal_secret)
|
|
)
|
|
if output_file.strip():
|
|
payload = PluginCommandFileAdapter.write_json_file(
|
|
payload,
|
|
output_file,
|
|
failure_message='插件配置导出失败',
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_config_text,
|
|
)
|
|
|
|
def _import_plugin_config(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
input_file: str,
|
|
) -> None:
|
|
"""
|
|
导入插件配置。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param input_file: 配置导入 JSON 文件路径
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
dry_run=False,
|
|
command_name='plugin config import',
|
|
)
|
|
values_payload = PluginCommandFileAdapter.read_config_import_file(input_file)
|
|
if not values_payload.get('ok', False):
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
{'pluginId': plugin_id, **values_payload},
|
|
text_builder=self.presenter.build_config_text,
|
|
default_exit_code=ARGUMENT_ERROR,
|
|
)
|
|
return
|
|
payload = self.execution_service.run_async(
|
|
self.core_runtime.import_plugin_config(plugin_id, values_payload.get('values', {}))
|
|
)
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_config_text,
|
|
)
|
|
|
|
def _set_plugin_config(
|
|
self,
|
|
plugin_id: str,
|
|
pairs: list[str],
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
更新插件配置。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param pairs: 配置键值列表
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
ctx = self.context_factory.build_dangerous(
|
|
env,
|
|
output,
|
|
allow_prod,
|
|
yes,
|
|
dry_run=False,
|
|
command_name='plugin config set',
|
|
)
|
|
try:
|
|
values = self._parse_config_pairs(pairs)
|
|
except ValueError as exc:
|
|
self.execution_service.complete_payload_with_text(
|
|
ctx,
|
|
{'ok': False, 'message': str(exc), 'pluginId': plugin_id},
|
|
text_builder=self.presenter.build_config_text,
|
|
default_exit_code=ARGUMENT_ERROR,
|
|
)
|
|
return
|
|
payload = self.execution_service.run_async(self.core_runtime.set_plugin_config(plugin_id, values))
|
|
payload['env'] = ctx.env
|
|
self._complete_plugin_payload(
|
|
ctx,
|
|
payload,
|
|
text_builder=self.presenter.build_config_text,
|
|
)
|
|
|
|
@staticmethod
|
|
def _parse_config_pairs(pairs: list[str]) -> dict[str, str]:
|
|
"""
|
|
解析配置键值参数。
|
|
|
|
:param pairs: 配置键值参数列表
|
|
:return: 配置键值字典
|
|
"""
|
|
values = {}
|
|
for pair in pairs:
|
|
if '=' not in pair:
|
|
raise ValueError(f'配置参数必须使用 key=value 格式:{pair}')
|
|
key, value = pair.split('=', 1)
|
|
try:
|
|
values[key] = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
values[key] = value
|
|
return values
|