Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/cli/groups/plugin/controller.py
T
insistence 2a055ba648 feat: 新增插件系统 (#112)
* 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: 更新后端依赖文件
2026-07-28 20:35:18 +08:00

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