Files

266 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}