// +---------------------------------------------------------------------- namespace ywxapp\command; use Exception; use think\console\Command; use think\console\Input; use think\console\input\Argument; use think\console\input\Option; use think\console\Output; use think\File; use ywxapp\exception\AddonException; use ywxapp\service\AddonService; use ywxapp\service\AddonHotReload; /** * 插件统一管理命令(合并 develop / install / reload) * * 用法: * php think addon:manage <插件名> -a develop * php think addon:manage <插件名> -a install [--local=] [--force] * php think addon:manage [<插件名>] -a reload [--force] [--status] * * 说明: * - develop / install / reload 逻辑内联于此; * - 菜单重载 / 数据库自愈 / 配置补全已由 addon:repair 统一负责(含 --menu 单项), * 故本命令不再保留 refresh-menu; * - health 逻辑体量大且复用 AddonHealth,故委托 ywxapp\command\AddonHealth 执行(长选项形式传参,规避短选项冲突)。 */ class AddonManage extends Command { /** * 配置命令 */ protected function configure() { $this->setName('addon:manage') ->addArgument('addon', Argument::OPTIONAL, '插件名(部分动作需要;留空语义见各动作)') ->addOption('action', 'a', Option::VALUE_REQUIRED, '动作: develop|install|reload') // install ->addOption('local', 'l', Option::VALUE_REQUIRED, '离线 zip 路径(install 离线安装)') ->addOption('force', 'f', Option::VALUE_NONE, '强制(install 在线强制覆盖 / reload 强制重载)') // reload ->addOption('status', 's', Option::VALUE_NONE, '查看重载状态(reload)') ->setDescription('插件统一管理:开发安装 / 安装 / 重导菜单 / 热重载'); } /** * 执行命令:按 -a 分发 */ protected function execute(Input $input, Output $output): int { $action = strtolower(trim((string) $input->getOption('action'))); if ($action === '') { $output->writeln('请通过 -a/--action 指定动作:develop|install|reload'); return 1; } switch ($action) { case 'develop': return $this->doDevelop($input, $output); case 'install': return $this->doInstall($input, $output); case 'reload': return $this->doReload($input, $output); default: $output->writeln('未知动作: ' . $action . '(支持 develop|install|reload;菜单重载请用 addon:repair --menu)'); return 1; } } /** * 开发者模式安装(源码已在 addon/,原地建表/注入菜单/启用,免打包) */ private function doDevelop(Input $input, Output $output): int { $name = trim((string) $input->getArgument('addon')); if ($name === '') { $output->writeln('请指定插件名,例如:php think addon:manage demo -a develop'); return 1; } try { $info = AddonService::instance($name)->developInstall(); $version = $info['version'] ?? ''; $output->writeln("开发模式安装成功:{$name} v{$version}"); $output->writeln('已建表、注入菜单并启用,可直接测试。'); } catch (AddonException $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } catch (\Exception $e) { $output->writeln('开发模式安装失败:' . $e->getMessage() . ''); return 1; } return 0; } /** * 安装插件:在线下载安装(默认)或离线 zip 安装 */ private function doInstall(Input $input, Output $output): int { $name = trim((string) $input->getArgument('addon')); if ($name === '' || !preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) { $output->writeln('请提供合法的插件名,例如:php think addon:manage demo -a install'); return 1; } $extend = [ 'install_user' => 0, 'install_ip' => 'cli', 'install_time' => time(), ]; try { $localOpt = (string) $input->getOption('local'); if ($localOpt !== '') { $local = realpath($localOpt); if ($local === false || !is_file($local)) { $output->writeln("Local zip not found: {$localOpt}"); return 1; } $output->writeln("Offline install from '{$local}' (addon='{$name}')..."); $file = new File($local); AddonService::instance($name)->local($file, $extend); $output->writeln("Addon '{$name}' offline-installed successfully."); return 0; } $force = (bool) $input->getOption('force'); $output->writeln("Online install '{$name}' (force=" . ($force ? 'yes' : 'no') . ")..."); AddonService::instance($name)->install($force, $extend); $output->writeln("Addon '{$name}' online-installed successfully."); return 0; } catch (AddonException $e) { $output->writeln('Install failed: ' . $e->getMessage() . ''); return 1; } catch (Exception $e) { $output->writeln('Install failed: ' . $e->getMessage() . ''); return 1; } } /** * 插件热重载(需 app_debug) */ private function doReload(Input $input, Output $output): int { if (!config('app.app_debug')) { $output->error('热重载功能仅在开发环境可用'); return 1; } $addon = trim((string) $input->getArgument('addon')); $force = (bool) $input->getOption('force'); $showStatus = (bool) $input->getOption('status'); if ($showStatus) { return $this->reloadStatus($addon, $output); } if ($addon) { return $this->reloadSingle($addon, $force, $output); } return $this->reloadAll($output); } private function reloadSingle(string $addon, bool $force, Output $output): int { $output->writeln("正在重载插件: {$addon}"); try { $reloaded = AddonHotReload::reloadAddon($addon, $force); if ($reloaded) { $output->writeln("插件 {$addon} 重载成功"); $status = AddonHotReload::getReloadStatus($addon); if (isset($status['files_changed'])) { $output->writeln('变更文件: ' . implode(', ', $status['files_changed'])); } } else { $output->writeln("插件 {$addon} 无文件变更,无需重载"); } return 0; } catch (\Exception $e) { $output->error('重载失败: ' . $e->getMessage()); return 1; } } private function reloadAll(Output $output): int { $output->writeln('正在重载所有已启用插件...'); try { $results = AddonHotReload::reloadAlladdon(); $successCount = 0; $reloadedCount = 0; $failedCount = 0; foreach ($results as $addon => $result) { if ($result['success']) { $successCount++; if ($result['reloaded']) { $reloadedCount++; $output->writeln("✓ {$addon} - 已重载"); } else { $output->writeln("- {$addon} - 无变更"); } } else { $failedCount++; $output->writeln("✗ {$addon} - {$result['error']}"); } } $output->writeln(''); $output->writeln('重载完成:'); $output->writeln(" 成功: {$successCount}"); $output->writeln(" 实际重载: {$reloadedCount}"); $output->writeln(" 失败: {$failedCount}"); return $failedCount > 0 ? 1 : 0; } catch (\Exception $e) { $output->error('重载失败: ' . $e->getMessage()); return 1; } } private function reloadStatus(string $addon, Output $output): int { if ($addon) { $status = AddonHotReload::getReloadStatus($addon); $output->writeln("插件 {$addon} 重载状态:"); $output->writeln(' 状态: ' . ($status['status'] ?? 'unknown')); if (isset($status['reload_time'])) { $output->writeln(' 最后重载时间: ' . date('Y-m-d H:i:s', $status['reload_time'])); } if (isset($status['error'])) { $output->writeln(' 错误信息: ' . $status['error']); } if (isset($status['files_changed'])) { $output->writeln(' 变更文件: ' . implode(', ', $status['files_changed'])); } } else { $stats = AddonHotReload::getReloadStatistics(); $output->writeln('插件热重载统计:'); $output->writeln(' 总重载次数: ' . ($stats['total_reloads'] ?? 0)); $output->writeln(' 成功次数: ' . ($stats['successful_reloads'] ?? 0)); $output->writeln(' 失败次数: ' . ($stats['failed_reloads'] ?? 0)); if (!empty($stats['addon'])) { $output->writeln(''); $output->writeln('各插件重载状态:'); foreach ($stats['addon'] as $name => $status) { $icon = ($status['status'] ?? '') === 'success' ? '✓' : '✗'; $color = ($status['status'] ?? '') === 'success' ? 'info' : 'error'; $output->writeln("<{$color}>{$icon} {$name}"); if (isset($status['reload_time'])) { $output->writeln(' 最后重载: ' . date('Y-m-d H:i:s', $status['reload_time'])); } } } } return 0; } }