266 lines
11 KiB
PHP
266 lines
11 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
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=<zip>] [--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('<error>请通过 -a/--action 指定动作:develop|install|reload</error>');
|
||
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('<error>未知动作: ' . $action . '(支持 develop|install|reload;菜单重载请用 addon:repair --menu)</error>');
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 开发者模式安装(源码已在 addon/<name>,原地建表/注入菜单/启用,免打包)
|
||
*/
|
||
private function doDevelop(Input $input, Output $output): int
|
||
{
|
||
$name = trim((string) $input->getArgument('addon'));
|
||
if ($name === '') {
|
||
$output->writeln('<error>请指定插件名,例如:php think addon:manage demo -a develop</error>');
|
||
return 1;
|
||
}
|
||
try {
|
||
$info = AddonService::instance($name)->developInstall();
|
||
$version = $info['version'] ?? '';
|
||
$output->writeln("<info>开发模式安装成功:{$name} v{$version}</info>");
|
||
$output->writeln('<info>已建表、注入菜单并启用,可直接测试。</info>');
|
||
} catch (AddonException $e) {
|
||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||
return 1;
|
||
} catch (\Exception $e) {
|
||
$output->writeln('<error>开发模式安装失败:' . $e->getMessage() . '</error>');
|
||
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('<error>请提供合法的插件名,例如:php think addon:manage demo -a install</error>');
|
||
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("<error>Local zip not found: {$localOpt}</error>");
|
||
return 1;
|
||
}
|
||
$output->writeln("<comment>Offline install from '{$local}' (addon='{$name}')...</comment>");
|
||
$file = new File($local);
|
||
AddonService::instance($name)->local($file, $extend);
|
||
$output->writeln("<info>Addon '{$name}' offline-installed successfully.</info>");
|
||
return 0;
|
||
}
|
||
|
||
$force = (bool) $input->getOption('force');
|
||
$output->writeln("<comment>Online install '{$name}' (force=" . ($force ? 'yes' : 'no') . ")...</comment>");
|
||
AddonService::instance($name)->install($force, $extend);
|
||
$output->writeln("<info>Addon '{$name}' online-installed successfully.</info>");
|
||
return 0;
|
||
} catch (AddonException $e) {
|
||
$output->writeln('<error>Install failed: ' . $e->getMessage() . '</error>');
|
||
return 1;
|
||
} catch (Exception $e) {
|
||
$output->writeln('<error>Install failed: ' . $e->getMessage() . '</error>');
|
||
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("<info>正在重载插件: {$addon}</info>");
|
||
try {
|
||
$reloaded = AddonHotReload::reloadAddon($addon, $force);
|
||
if ($reloaded) {
|
||
$output->writeln("<info>插件 {$addon} 重载成功</info>");
|
||
$status = AddonHotReload::getReloadStatus($addon);
|
||
if (isset($status['files_changed'])) {
|
||
$output->writeln('变更文件: ' . implode(', ', $status['files_changed']));
|
||
}
|
||
} else {
|
||
$output->writeln("<comment>插件 {$addon} 无文件变更,无需重载</comment>");
|
||
}
|
||
return 0;
|
||
} catch (\Exception $e) {
|
||
$output->error('重载失败: ' . $e->getMessage());
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
private function reloadAll(Output $output): int
|
||
{
|
||
$output->writeln('<info>正在重载所有已启用插件...</info>');
|
||
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("<info>✓ {$addon} - 已重载</info>");
|
||
} else {
|
||
$output->writeln("<comment>- {$addon} - 无变更</comment>");
|
||
}
|
||
} else {
|
||
$failedCount++;
|
||
$output->writeln("<error>✗ {$addon} - {$result['error']}</error>");
|
||
}
|
||
}
|
||
$output->writeln('');
|
||
$output->writeln('<info>重载完成:</info>');
|
||
$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("<info>插件 {$addon} 重载状态:</info>");
|
||
$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('<info>插件热重载统计:</info>');
|
||
$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('<info>各插件重载状态:</info>');
|
||
foreach ($stats['addon'] as $name => $status) {
|
||
$icon = ($status['status'] ?? '') === 'success' ? '✓' : '✗';
|
||
$color = ($status['status'] ?? '') === 'success' ? 'info' : 'error';
|
||
$output->writeln("<{$color}>{$icon} {$name}</{$color}>");
|
||
if (isset($status['reload_time'])) {
|
||
$output->writeln(' 最后重载: ' . date('Y-m-d H:i:s', $status['reload_time']));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
}
|