Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/cli/groups/plugin/presenter.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

925 lines
38 KiB
Python

from cli.utils import SHELL_TEXT_FORMATTER
class PluginCommandPresenter:
"""
插件命令文本渲染器。
"""
def build_list_text(self, payload: dict[str, object]) -> str:
"""
将插件列表负载渲染为文本。
:param payload: 插件列表负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'count: {payload.get("count", 0)}',
]
if 'databaseAvailable' in payload:
database_available = bool(payload.get('databaseAvailable', False))
lines.append(f'database_available: {str(database_available).lower()}')
if not database_available:
database_error = SHELL_TEXT_FORMATTER.truncate_text(payload.get('databaseError', '') or '', 120)
lines.append(f'database_error: {database_error or "-"}')
plugins = payload.get('plugins')
if not isinstance(plugins, list) or not plugins:
lines.append('plugins: none')
return '\n'.join(lines)
lines.append('plugins:')
lines.extend(self._build_plugin_summary_line(plugin) for plugin in plugins if isinstance(plugin, dict))
return '\n'.join(lines)
def build_info_text(self, payload: dict[str, object]) -> str:
"""
将插件详情负载渲染为文本。
:param payload: 插件详情负载
:return: 文本输出
"""
plugin = payload.get('plugin')
if not isinstance(plugin, dict):
return '\n'.join(
[
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
]
)
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'plugin_id: {plugin.get("pluginId", "-")}',
f'name: {plugin.get("name", "-")}',
f'version: {plugin.get("version", "-")}',
f'installed_version: {plugin.get("installedVersion", "-") or "-"}',
f'runtime_enabled: {str(plugin.get("runtimeEnabled", False)).lower()}',
f'status: {plugin.get("status", "-")}',
f'source: {plugin.get("source", "-")}',
f'last_error: {SHELL_TEXT_FORMATTER.truncate_text(plugin.get("lastError", "") or "", 100) or "-"}',
f'description: {SHELL_TEXT_FORMATTER.truncate_text(plugin.get("description", ""), 100)}',
f'backend_path: {SHELL_TEXT_FORMATTER.truncate_text(plugin.get("backendPath", ""), 120)}',
f'frontend_path: {SHELL_TEXT_FORMATTER.truncate_text(plugin.get("frontendPath", "") or "", 120) or "-"}',
f'menu_count: {plugin.get("menuCount", 0)}',
f'permission_count: {plugin.get("permissionCount", 0)}',
]
database = plugin.get('database')
if isinstance(database, dict):
lines.append(f'database_available: {str(database.get("available", False)).lower()}')
lines.append(f'database_installed: {str(database.get("installed", False)).lower()}')
if 'configuredEnabled' in database:
lines.append(f'configured_enabled: {str(database.get("configuredEnabled", False)).lower()}')
dependencies = plugin.get('dependencies')
if isinstance(dependencies, list):
lines.append(f'dependencies: {len(dependencies)}')
lines.extend(self._build_dependency_lines(dependencies))
return '\n'.join(lines)
def build_check_text(self, payload: dict[str, object]) -> str:
"""
将插件检查负载渲染为文本。
:param payload: 插件检查负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'count: {payload.get("count", 0)}',
]
checks = payload.get('checks')
if not isinstance(checks, list) or not checks:
lines.append('checks: none')
return '\n'.join(lines)
lines.append('checks:')
lines.extend(self._build_check_summary_line(check) for check in checks if isinstance(check, dict))
return '\n'.join(lines)
def build_dependency_text(self, payload: dict[str, object]) -> str:
"""
将插件依赖检查负载渲染为文本。
:param payload: 插件依赖检查负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'dependency_ok: {str(payload.get("dependencyOk", False)).lower()}',
]
dependencies = payload.get('dependencies')
if isinstance(dependencies, list):
lines.append(f'dependencies: {len(dependencies)}')
lines.extend(self._build_dependency_lines(dependencies))
else:
lines.append('dependencies: none')
return '\n'.join(lines)
def build_precheck_text(self, payload: dict[str, object]) -> str:
"""
将插件操作预检负载渲染为文本。
:param payload: 插件操作预检负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'operation: {payload.get("operation", "-")}',
f'dependency_ok: {str(payload.get("dependencyOk", False)).lower()}',
f'plugin_dependency_ok: {str(payload.get("pluginDependencyOk", False)).lower()}',
f'structure_ok: {str(payload.get("structureOk", False)).lower()}',
f'menu_conflict_ok: {str(payload.get("menuConflictOk", False)).lower()}',
f'database_available: {str(payload.get("databaseAvailable", False)).lower()}',
]
actions = payload.get('actions')
lines.append(f'actions: {len(actions) if isinstance(actions, list) else 0}')
precheck = payload.get('precheck')
if isinstance(precheck, dict):
lines.append(f'dependencies: {len(precheck.get("dependencies", []))}')
lines.append(f'structure_errors: {len(precheck.get("structureErrors", []))}')
lines.append(f'menu_conflicts: {len(precheck.get("menuConflicts", []))}')
return '\n'.join(lines)
def build_health_text(self, payload: dict[str, object]) -> str:
"""
将插件健康检查负载渲染为文本。
:param payload: 插件健康检查负载
:return: 文本输出
"""
health = payload.get('health')
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
]
if not isinstance(health, dict):
return '\n'.join(lines)
lines.extend(
[
f'status: {health.get("status", "-")}',
f'checker: {health.get("checker", "-") or "-"}',
f'duration_ms: {health.get("durationMs", 0)}',
]
)
if health.get('error'):
lines.append(f'error: {SHELL_TEXT_FORMATTER.truncate_text(health.get("error", ""), 120)}')
details = health.get('details')
if isinstance(details, dict):
lines.append(f'details: {len(details)}')
return '\n'.join(lines)
def build_diagnose_text(self, payload: dict[str, object]) -> str:
"""
将插件诊断包负载渲染为文本。
:param payload: 插件诊断包负载
:return: 文本输出
"""
info = payload.get('info')
check = payload.get('check')
menu_plan = payload.get('menuPlan')
config = payload.get('config')
audit = payload.get('audit')
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
]
if isinstance(info, dict):
lines.append(f'status: {info.get("status", "-")}')
lines.append(f'installed_version: {info.get("installedVersion", "-") or "-"}')
lines.append(
f'last_error: {SHELL_TEXT_FORMATTER.truncate_text(info.get("lastError", "") or "", 100) or "-"}'
)
if isinstance(check, dict):
checks = check.get('checks')
check_items = checks if isinstance(checks, list) else []
first_check = check_items[0] if check_items and isinstance(check_items[0], dict) else {}
lines.append(f'check_ok: {str(check.get("ok", False)).lower()}')
lines.append(f'dependency_count: {len(first_check.get("dependencies", [])) if first_check else 0}')
lines.append(f'structure_errors: {len(first_check.get("structureErrors", [])) if first_check else 0}')
lines.append(f'menu_conflicts: {len(first_check.get("menuConflicts", [])) if first_check else 0}')
if isinstance(menu_plan, dict):
lines.append(
f'menu_plan: total={menu_plan.get("total", 0)} | '
f'permissions={menu_plan.get("permissionCount", 0)} | '
f'enabled={menu_plan.get("enabledCount", 0)}'
)
if isinstance(config, dict):
configs = config.get('configs')
lines.append(f'configs: {len(configs) if isinstance(configs, list) else 0}')
summary = config.get('summary')
if isinstance(summary, dict):
lines.append(
f'config_summary: total={summary.get("total", 0)} | '
f'secret={summary.get("secretCount", 0)} | '
f'missing_required={summary.get("missingRequiredCount", 0)}'
)
if isinstance(audit, dict):
lines.append(f'audit_available: {str(audit.get("available", False)).lower()}')
if payload.get('outputFile'):
lines.append(f'output_file: {payload.get("outputFile")}')
lines.append(f'exported: {str(payload.get("exported", False)).lower()}')
return '\n'.join(lines)
def build_docs_text(self, payload: dict[str, object]) -> str:
"""
将插件文档生成负载渲染为文本。
:param payload: 插件文档生成负载
:return: 文本输出
"""
if payload.get('ok') and isinstance(payload.get('markdown'), str) and not payload.get('outputFile'):
return str(payload.get('markdown', ''))
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'format: {payload.get("format", "-")}',
f'length: {payload.get("length", 0)}',
]
if payload.get('outputFile'):
lines.append(f'output_file: {payload.get("outputFile")}')
lines.append(f'exported: {str(payload.get("exported", False)).lower()}')
return '\n'.join(lines)
def build_test_text(self, payload: dict[str, object]) -> str:
"""
将插件测试负载渲染为文本。
:param payload: 插件测试负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'keyword: {payload.get("keyword", "") or "-"}',
f'maxfail: {payload.get("maxfail", 0)}',
f'quiet: {str(payload.get("quiet", False)).lower()}',
f'frontend_build: {str(payload.get("frontendBuild", False)).lower()}',
]
targets = payload.get('targets')
if isinstance(targets, list):
lines.append(f'targets: {len(targets)}')
lines.extend(f' - {target}' for target in targets)
command = payload.get('command')
if isinstance(command, list):
lines.append(f'command: {" ".join(str(item) for item in command)}')
results = payload.get('results')
if isinstance(results, list):
lines.append(f'results: {len(results)}')
for item in results:
if not isinstance(item, dict):
continue
test_result = item.get('test') if isinstance(item.get('test'), dict) else {}
lines.append(
f' - {item.get("kind", "-")}: {item.get("target", "-")} [{test_result.get("returnCode", "-")}]'
)
test_payload = payload.get('test')
if isinstance(test_payload, dict):
lines.append(f'return_code: {test_payload.get("returnCode", "-")}')
stdout = str(test_payload.get('stdout', '') or '').strip()
stderr = str(test_payload.get('stderr', '') or '').strip()
if stdout:
lines.append(f'stdout: {SHELL_TEXT_FORMATTER.truncate_text(stdout, 300)}')
if stderr:
lines.append(f'stderr: {SHELL_TEXT_FORMATTER.truncate_text(stderr, 300)}')
return '\n'.join(lines)
def build_dependency_install_text(self, payload: dict[str, object]) -> str:
"""
将插件依赖安装负载渲染为文本。
:param payload: 插件依赖安装负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'plan_count: {payload.get("planCount", 0)}',
]
plan = payload.get('plan')
if isinstance(plan, list) and plan:
lines.append('plan:')
lines.extend(self._build_dependency_plan_line(item) for item in plan if isinstance(item, dict))
else:
lines.append('plan: none')
results = payload.get('results')
if isinstance(results, list):
lines.append(f'results: {len(results)}')
policy = payload.get('policy')
if isinstance(policy, dict):
lines.extend(
[
f'policy_mode: {policy.get("mode", "-")}',
f'policy_allowed: {str(policy.get("allowed", False)).lower()}',
]
)
reasons = policy.get('reasons')
if isinstance(reasons, list) and reasons:
lines.append('policy_reasons:')
lines.extend(f' - {reason}' for reason in reasons)
requirements = policy.get('requirements')
if isinstance(requirements, list) and requirements:
lines.append('policy_requirements:')
lines.extend(f' - {requirement}' for requirement in requirements)
return '\n'.join(lines)
def build_dependency_lock_text(self, payload: dict[str, object]) -> str:
"""
将插件依赖锁文件模板负载渲染为文本。
:param payload: 插件依赖锁文件模板负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'output_file: {payload.get("outputFile", "-")}',
f'written: {str(payload.get("written", False)).lower()}',
f'overwritten: {str(payload.get("overwritten", False)).lower()}',
f'entry_count: {payload.get("entryCount", 0)}',
f'artifact_count: {payload.get("artifactCount", 0)}',
]
warnings = payload.get('warnings')
if isinstance(warnings, list) and warnings:
lines.append('warnings:')
lines.extend(f' - {warning}' for warning in warnings)
return '\n'.join(lines)
def build_dependency_allowlist_example_text(self, payload: dict[str, object]) -> str:
"""
将插件依赖允许列表示例负载渲染为文本。
:param payload: 插件依赖允许列表示例负载
:return: 文本输出
"""
if payload.get('ok') and isinstance(payload.get('allowlist'), str) and payload.get('dryRun'):
return str(payload.get('allowlist', ''))
return '\n'.join(
[
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'output_file: {payload.get("outputFile", "-")}',
f'written: {str(payload.get("written", False)).lower()}',
f'overwritten: {str(payload.get("overwritten", False)).lower()}',
]
)
def build_plan_text(self, payload: dict[str, object]) -> str:
"""
将插件批量操作拓扑计划负载渲染为文本。
:param payload: 插件批量操作拓扑计划负载
:return: 文本输出
"""
plan = payload.get('plan')
items = plan.get('items') if isinstance(plan, dict) else None
blockers = plan.get('blockers') if isinstance(plan, dict) else None
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'operation: {payload.get("operation", "-")}',
f'blocker_count: {plan.get("blockerCount", 0) if isinstance(plan, dict) else 0}',
]
if isinstance(items, list) and items:
lines.append('plan:')
lines.extend(self._build_plugin_plan_line(item) for item in items if isinstance(item, dict))
else:
lines.append('plan: none')
if isinstance(blockers, list) and blockers:
lines.append('blockers:')
lines.extend(self._build_plugin_plan_blocker_line(item) for item in blockers if isinstance(item, dict))
else:
lines.append('blockers: none')
return '\n'.join(lines)
def build_batch_text(self, payload: dict[str, object]) -> str:
"""
将插件批量执行负载渲染为文本。
:param payload: 插件批量执行负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'operation: {payload.get("operation", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'continue_on_error: {str(payload.get("continueOnError", False)).lower()}',
]
plan = payload.get('plan')
if isinstance(plan, dict):
lines.append(f'plan_items: {len(plan.get("items", []))}')
lines.append(f'blocker_count: {plan.get("blockerCount", 0)}')
summary = payload.get('summary')
if isinstance(summary, dict):
lines.append(
f'summary: total={summary.get("total", 0)} | succeeded={summary.get("succeeded", 0)} | '
f'failed={summary.get("failed", 0)} | skipped={summary.get("skipped", 0)}'
)
executed = payload.get('executed')
if isinstance(executed, list) and executed:
lines.append('executed:')
lines.extend(self._build_batch_result_line(item) for item in executed if isinstance(item, dict))
else:
lines.append('executed: none')
failed = payload.get('failed')
if isinstance(failed, dict):
lines.append(
f'failed: {failed.get("pluginId", "-")} | '
f'operation: {failed.get("operation", "-")} | message: {failed.get("message", "-")}'
)
if failed.get('suggestion'):
lines.append(f'suggestion: {failed.get("suggestion", "-")}')
else:
lines.append('failed: none')
return '\n'.join(lines)
def build_create_text(self, payload: dict[str, object]) -> str:
"""
将插件创建负载渲染为文本。
:param payload: 插件创建负载
:return: 文本输出
"""
lines = [
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'template: {payload.get("template", "-")}',
f'backend: {str(payload.get("backend", False)).lower()}',
f'frontend: {str(payload.get("frontend", False)).lower()}',
f'frontend_version: {payload.get("frontendVersion") or "-"}',
f'migration: {str(payload.get("migration", False)).lower()}',
f'seed: {str(payload.get("seed", False)).lower()}',
f'job: {str(payload.get("job", False)).lower()}',
f'config: {str(payload.get("config", False)).lower()}',
f'test: {str(payload.get("test", False)).lower()}',
]
conflicts = payload.get('conflicts')
if isinstance(conflicts, list) and conflicts:
lines.append('conflicts:')
lines.extend(f' - {conflict}' for conflict in conflicts)
files = payload.get('files')
if not isinstance(files, list) or not files:
lines.append('files: none')
return '\n'.join(lines)
lines.append(f'files: {len(files)}')
lines.extend(f' - {file_payload.get("path", "-")}' for file_payload in files if isinstance(file_payload, dict))
return '\n'.join(lines)
def build_install_text(self, payload: dict[str, object]) -> str:
"""
将插件安装负载渲染为文本。
:param payload: 插件安装负载
:return: 文本输出
"""
lines = [
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'dependency_ok: {str(payload.get("dependencyOk", False)).lower()}',
f'structure_ok: {str(payload.get("structureOk", True)).lower()}',
f'menu_conflict_ok: {str(payload.get("menuConflictOk", True)).lower()}',
]
actions = payload.get('actions')
if isinstance(actions, list) and actions:
lines.append('actions:')
lines.extend(self._build_action_summary_line(action) for action in actions if isinstance(action, dict))
else:
lines.append('actions: none')
dependencies = payload.get('dependencies')
if isinstance(dependencies, list) and dependencies:
lines.append(f'dependencies: {len(dependencies)}')
else:
lines.append('dependencies: none')
structure_errors = payload.get('structureErrors')
if isinstance(structure_errors, list) and structure_errors:
lines.append(f'structure_errors: {len(structure_errors)}')
else:
lines.append('structure_errors: none')
menu_conflicts = payload.get('menuConflicts')
if isinstance(menu_conflicts, list) and menu_conflicts:
lines.append(f'menu_conflicts: {len(menu_conflicts)}')
else:
lines.append('menu_conflicts: none')
self._append_lifecycle_migration_lines(lines, payload)
return '\n'.join(lines)
def build_upgrade_text(self, payload: dict[str, object]) -> str:
"""
将插件升级负载渲染为文本。
:param payload: 插件升级负载
:return: 文本输出
"""
lines = [
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'installed: {str(payload.get("installed", False)).lower()}',
f'installed_version: {payload.get("installedVersion", "-") or "-"}',
f'current_version: {payload.get("currentVersion", "-") or "-"}',
f'needs_upgrade: {str(payload.get("needsUpgrade", False)).lower()}',
f'database_available: {str(payload.get("databaseAvailable", True)).lower()}',
f'dependency_ok: {str(payload.get("dependencyOk", False)).lower()}',
f'structure_ok: {str(payload.get("structureOk", True)).lower()}',
f'menu_conflict_ok: {str(payload.get("menuConflictOk", True)).lower()}',
]
actions = payload.get('actions')
if isinstance(actions, list) and actions:
lines.append('actions:')
lines.extend(self._build_action_summary_line(action) for action in actions if isinstance(action, dict))
else:
lines.append('actions: none')
structure_errors = payload.get('structureErrors')
if isinstance(structure_errors, list) and structure_errors:
lines.append(f'structure_errors: {len(structure_errors)}')
else:
lines.append('structure_errors: none')
menu_conflicts = payload.get('menuConflicts')
if isinstance(menu_conflicts, list) and menu_conflicts:
lines.append(f'menu_conflicts: {len(menu_conflicts)}')
else:
lines.append('menu_conflicts: none')
self._append_lifecycle_migration_lines(lines, payload)
return '\n'.join(lines)
def build_enabled_text(self, payload: dict[str, object]) -> str:
"""
将插件启停负载渲染为文本。
:param payload: 插件启停负载
:return: 文本输出
"""
lines = [
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'operation: {payload.get("operation", "-")}',
f'target_enabled: {str(payload.get("targetEnabled", False)).lower()}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
]
actions = payload.get('actions')
if isinstance(actions, list) and actions:
lines.append('actions:')
lines.extend(self._build_action_summary_line(action) for action in actions if isinstance(action, dict))
else:
lines.append('actions: none')
return '\n'.join(lines)
def build_purge_text(self, payload: dict[str, object]) -> str:
"""
将插件物理清理负载渲染为文本。
:param payload: 插件物理清理负载
:return: 文本输出
"""
plan = payload.get('plan')
plan_items = plan.get('items') if isinstance(plan, dict) else None
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'operation: {payload.get("operation", "-")}',
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
f'safe_mode: {str(payload.get("safeMode", False)).lower()}',
f'removes_source: {str(payload.get("removesSource", False)).lower()}',
f'destructive_count: {plan.get("destructiveCount", 0) if isinstance(plan, dict) else 0}',
]
if isinstance(plan_items, list) and plan_items:
lines.append('plan:')
lines.extend(self._build_purge_plan_line(item) for item in plan_items if isinstance(item, dict))
else:
lines.append('plan: none')
hooks = payload.get('hooks')
if isinstance(hooks, list) and hooks:
lines.append(f'hooks: {len(hooks)}')
else:
lines.append('hooks: none')
return '\n'.join(lines)
def build_migration_list_text(self, payload: dict[str, object]) -> str:
"""
将插件 migration 历史负载渲染为文本。
:param payload: 插件 migration 历史负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'status: {payload.get("status", "-") or "-"}',
f'count: {payload.get("count", 0)}',
]
migrations = payload.get('migrations')
if not isinstance(migrations, list) or not migrations:
lines.append('migrations: none')
return '\n'.join(lines)
lines.append('migrations:')
lines.extend(
self._build_migration_summary_line(migration) for migration in migrations if isinstance(migration, dict)
)
return '\n'.join(lines)
def build_migration_mark_text(self, payload: dict[str, object]) -> str:
"""
将插件 migration 人工标记负载渲染为文本。
:param payload: 插件 migration 人工标记负载
:return: 文本输出
"""
return '\n'.join(
[
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
f'env: {payload.get("env", "-")}',
f'operation: {payload.get("operation", "-")}',
f'migration_path: {payload.get("migrationPath", "-")}',
f'status: {payload.get("status", "-")}',
]
)
def build_config_text(self, payload: dict[str, object]) -> str:
"""
将插件配置负载渲染为文本。
:param payload: 插件配置负载
:return: 文本输出
"""
lines = [
f'ok: {str(payload.get("ok", False)).lower()}',
f'message: {payload.get("message", "-")}',
f'plugin_id: {payload.get("pluginId", "-")}',
]
configs = payload.get('configs')
if not isinstance(configs, list) or not configs:
lines.append('configs: none')
return '\n'.join(lines)
lines.append(f'configs: {len(configs)}')
lines.extend(self._build_config_summary_line(config) for config in configs if isinstance(config, dict))
return '\n'.join(lines)
@staticmethod
def _build_dependency_lines(dependencies: list[object]) -> list[str]:
"""
构建依赖检查文本行。
:param dependencies: 依赖检查负载列表
:return: 文本行列表
"""
if not dependencies:
return ['dependencies_detail: none']
lines = ['dependencies_detail:']
lines.extend(
PluginCommandPresenter._build_dependency_summary_line(dependency)
for dependency in dependencies
if isinstance(dependency, dict)
)
return lines
@staticmethod
def _build_plugin_summary_line(plugin: dict[str, object]) -> str:
"""
构建插件摘要文本行。
:param plugin: 插件摘要负载
:return: 文本行
"""
return (
f' - {plugin.get("pluginId", "-")} | {plugin.get("name", "-")} | '
f'version: {plugin.get("version", "-")} | '
f'runtime_enabled: {str(plugin.get("runtimeEnabled", False)).lower()} | '
f'status: {plugin.get("status", "-")}'
)
@staticmethod
def _build_config_summary_line(config: dict[str, object]) -> str:
"""
构建插件配置摘要文本行。
:param config: 插件配置负载
:return: 文本行
"""
return (
f' - {config.get("key", "-")} | {config.get("label", "-") or "-"} | '
f'type: {config.get("type", "-")} | value: {config.get("value", "-")}'
)
@staticmethod
def _build_migration_summary_line(migration: dict[str, object]) -> str:
"""
构建插件 migration 历史摘要文本行。
:param migration: migration 历史负载
:return: 文本行
"""
checksum = str(migration.get('migrationChecksum') or migration.get('checksum') or '')
migration_path = migration.get('migrationPath') or migration.get('migration_path') or '-'
status = migration.get('status', '-')
attempts = migration.get('attemptCount', migration.get('attempt_count', 0))
version = migration.get('version', '-') or '-'
duration_ms = migration.get('durationMs', migration.get('duration_ms'))
duration_text = f' | duration_ms: {duration_ms}' if duration_ms is not None else ''
return (
f' - {migration_path} | status: {status} | attempts: {attempts} | version: {version} | '
f'checksum: {checksum[:12] or "-"}{duration_text}'
)
def _append_lifecycle_migration_lines(self, lines: list[str], payload: dict[str, object]) -> None:
"""
追加生命周期 migration 结果和恢复建议文本。
:param lines: 文本行列表
:param payload: 生命周期负载
:return: None
"""
migration_recovery = payload.get('migrationRecovery')
if isinstance(migration_recovery, dict):
lines.append(
'migration_recovery: '
f'{migration_recovery.get("migrationPath", "-")} | '
f'status: {migration_recovery.get("status", "-")} | '
f'suggestion: {migration_recovery.get("suggestion", "-")}'
)
migrations = payload.get('migrations')
if not isinstance(migrations, list) or not migrations:
lines.append('migrations: none')
return
lines.append('migrations:')
lines.extend(
self._build_migration_summary_line(migration) for migration in migrations if isinstance(migration, dict)
)
@staticmethod
def _build_dependency_plan_line(item: dict[str, object]) -> str:
"""
构建依赖安装计划文本行。
:param item: 依赖安装计划项
:return: 文本行
"""
return (
f' - {item.get("kind", "-")} | {item.get("requirement", "-")} | '
f'workdir: {item.get("workdir", "-")} | command: {item.get("commandText", "-")}'
)
@staticmethod
def _build_purge_plan_line(item: dict[str, object]) -> str:
"""
构建插件物理清理计划文本行。
:param item: 插件物理清理计划项
:return: 文本行
"""
will_run = item.get('willRun', item.get('enabled', False))
return (
f' - {item.get("name", "-")} | will_run: {str(will_run).lower()} | '
f'destructive: {str(item.get("destructive", False)).lower()} | '
f'count: {item.get("count", "-") if item.get("count") is not None else "-"} | '
f'label: {item.get("label", "-")}'
)
@staticmethod
def _build_plugin_plan_line(item: dict[str, object]) -> str:
"""
构建插件批量操作计划文本行。
:param item: 插件批量操作计划项
:return: 文本行
"""
return (
f' - #{item.get("order", "-")} {item.get("pluginId", "-")} | '
f'ready: {str(item.get("ready", False)).lower()} | '
f'requested: {str(item.get("requested", False)).lower()} | '
f'deps: {len(item.get("dependencies", []))}'
)
@staticmethod
def _build_plugin_plan_blocker_line(item: dict[str, object]) -> str:
"""
构建插件批量操作计划阻塞项文本行。
:param item: 插件批量操作计划阻塞项
:return: 文本行
"""
return (
f' - {item.get("pluginId", "-")} -> {item.get("dependencyId", "-")} | '
f'status: {item.get("status", "-")} | message: {item.get("message", "-")}'
)
@staticmethod
def _build_batch_result_line(item: dict[str, object]) -> str:
"""
构建插件批量执行结果文本行。
:param item: 插件批量执行结果项
:return: 文本行
"""
return (
f' - {item.get("pluginId", "-")} | operation: {item.get("operation", "-")} | '
f'ok: {str(item.get("ok", False)).lower()} | status: {item.get("status", "-")} | '
f'duration_ms: {item.get("durationMs", 0)} | message: {item.get("message", "-")}'
)
@staticmethod
def _build_check_summary_line(check: dict[str, object]) -> str:
"""
构建插件检查摘要文本行。
:param check: 插件检查负载
:return: 文本行
"""
missing_dependencies = check.get('missingDependencies', [])
unsatisfied_dependencies = check.get('unsatisfiedDependencies', [])
structure_errors = check.get('structureErrors', [])
menu_conflicts = check.get('menuConflicts', [])
return (
f' - {check.get("pluginId", "-")} | ok: {str(check.get("ok", False)).lower()} | '
f'missing: {len(missing_dependencies)} | unsatisfied: {len(unsatisfied_dependencies)} | '
f'structure_errors: {len(structure_errors)} | menu_conflicts: {len(menu_conflicts)}'
)
@staticmethod
def _build_dependency_summary_line(dependency: dict[str, object]) -> str:
"""
构建依赖检查摘要文本行。
:param dependency: 依赖检查负载
:return: 文本行
"""
return (
f' - {dependency.get("kind", "-")}:{dependency.get("name", "-")} | '
f'ok: {str(dependency.get("ok", False)).lower()} | '
f'required: {dependency.get("requiredVersion", "-") or "-"} | '
f'installed: {dependency.get("installedVersion", "-") or "-"}'
)
@staticmethod
def _build_action_summary_line(action: dict[str, object]) -> str:
"""
构建安装动作摘要文本行。
:param action: 安装动作负载
:return: 文本行
"""
will_run = action.get('willRun', action.get('enabled', False))
return f' - {action.get("name", "-")} | will_run: {str(will_run).lower()} | label: {action.get("label", "-")}'