input('uuid', $sessionUuid); } // 检查插件是否已安装(检查 installed.php 文件) $pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $code; $installedConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php'; if (is_file($installedConfigFile)) { yield Sse::warning('插件已安装,跳过安装', ['plugin' => $code], $sessionUuid); return; } try { yield Sse::progress(PluginInstallEvent::INSTALL_START->label(), 0, ['plugin' => $code], $sessionUuid); // 1. 检查本地是否已经下载了模块 yield Sse::progress('检查本地模块', 5, [], $sessionUuid); $localPluginPath = $this->plugin_path . DIRECTORY_SEPARATOR . $code; $moduleExists = is_dir($localPluginPath); // 2. 如果是local模式,只检测plugin目录是否包含,没有则直接返回异常安装失败 if ($mode === 'local') { if (!$moduleExists) { yield Sse::error('本地模块不存在,安装失败', [], $sessionUuid); return; } yield Sse::progress('本地模块存在', 10, [], $sessionUuid); } else { // 3. 如果没有指定,优先检测本地,没有则通过远程下载模块zip if (!$moduleExists) { yield Sse::progress('本地模块不存在,开始远程下载', 10, [], $sessionUuid); // 获取下载链接 $remoteService = Container::make(PluginRemoteService::class); $authCode = config('madong.auth_code'); $secretKey = config('madong.auth_secret'); // 检查授权配置 if (empty($authCode) || empty($secretKey)) { yield Sse::error('授权配置缺失,无法下载远程插件。请在配置文件中设置 madong.auth_code 和 madong.auth_secret', [], $sessionUuid); return; } $downloadInfo = $remoteService->getRemoteDownloadService($authCode, $secretKey, $code, 'latest'); // 记录下载信息,方便调试 yield Sse::progress('下载接口返回数据: ' . json_encode($downloadInfo, JSON_UNESCAPED_UNICODE), 12, [], $sessionUuid); if (empty($downloadInfo['url'])) { $errorMsg = '获取下载链接失败'; if (isset($downloadInfo['msg'])) { $errorMsg .= ': ' . $downloadInfo['msg']; } if (isset($downloadInfo['message'])) { $errorMsg .= ': ' . $downloadInfo['message']; } yield Sse::error($errorMsg, [], $sessionUuid); return; } yield Sse::progress('获取下载链接成功: ' . $downloadInfo['url'], 15, [], $sessionUuid); // 下载并解压插件 yield Sse::progress('开始下载并解压模块', 20, [], $sessionUuid); $downloadService = Container::make(PluginDownloadService::class); $downloadService->downloadAndExtract($code, $downloadInfo['url']); yield Sse::progress('模块下载并解压完成', 30, [], $sessionUuid); } else { yield Sse::progress('本地模块存在', 20, [], $sessionUuid); } } // 5. 安装检查 yield Sse::progress(PluginInstallEvent::INSTALL_CHECK->label(), 35, [], $sessionUuid); $checkResult = $this->installCheck($code); if ($checkResult['overall_status'] !== 'success') { yield Sse::error($checkResult['message'], [], $sessionUuid); return; } Monitor::pause(); // 6. 统一调用插件 Install.php 进行安装(这是唯一的安装入口) yield Sse::progress(PluginInstallEvent::EXECUTE_INSTALL_METHOD->label(), 50, [], $sessionUuid); $pluginConfig = $this->getPluginConfig($code); $version = $pluginConfig['version'] ?? '1.0.0'; // 检查插件是否提供 Install.php $className = "plugin\\{$code}\\Install"; if (!class_exists($className)) { throw new PluginException("插件未提供 Install.php,无法进行安装"); } // 实例化并执行插件 Install 类(传入进度回调以支持在线模式反馈) $installInstance = new $className(); // 创建临时输出文件 $outputFile = runtime_path(self::RUNTIME_PLUGIN_PATH . '/install_' . $sessionUuid . '.log'); // 确保目录存在 $outputDir = dirname($outputFile); if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); } file_put_contents($outputFile, ''); // 定义进度回调,将插件的安装进度实时写入文件 $callbackCount = 0; $progressCallback = function(string $message, ?int $progress = null) use ($outputFile, &$callbackCount) { $callbackCount++; $progressStr = $progress !== null ? "(progress: {$progress})" : "(no progress)"; $logLine = "[CALLBACK #{$callbackCount}] {$message} {$progressStr}\n"; // 写入文件(追加模式),不输出到控制台避免重复 file_put_contents($outputFile, $logLine, FILE_APPEND); }; // 设置进度回调和在线模式 $installInstance->setProgressCallback($progressCallback); $installInstance->setOnlineMode(true); // 开启输出缓冲,捕获 Install::install() 内部的 echo 输出 ob_start(); try { $installInstance->install($version); } catch (\Throwable $e) { ob_end_clean(); @unlink($outputFile); // 清理临时文件 yield Sse::error('插件安装执行失败: ' . $e->getMessage(), [ 'exception' => get_class($e), 'file' => $e->getFile(), 'line' => $e->getLine(), ], $sessionUuid); return; } $output = ob_get_clean(); // 将缓冲区输出也写入临时文件 if (!empty($output)) { file_put_contents($outputFile, $output, FILE_APPEND); } // 读取文件内容并 yield 所有日志 $fileContent = file_get_contents($outputFile); if (!empty($fileContent)) { $lines = array_filter(explode("\n", $fileContent)); foreach ($lines as $line) { $line = trim($line); if (!empty($line)) { yield Sse::progress($line, 50, [], $sessionUuid); } } } // 清理临时文件 @unlink($outputFile); yield Sse::progress('插件安装方法执行成功', 85, [], $sessionUuid); // 8. 创建安装标记文件 yield Sse::progress('创建安装标记', 90, [], $sessionUuid); $installedConfig = [ 'version' => $pluginConfig['version'] ?? '1.0.0', 'installed_at' => date('Y-m-d H:i:s'), ]; file_put_contents($installedConfigFile, 'label(), ['plugin' => $code], $sessionUuid); Monitor::pause(); } catch (PluginException $e) { yield Sse::error($e->getMessage(), [], $sessionUuid); return; } catch (\Exception $e) { yield Sse::error('安装失败:' . $e->getMessage(), [], $sessionUuid); return; } } /** * 安装检查 * * @param string $name 插件名称 * * @return array 检查结果 */ public function installCheck(string $name): array { // 初始化返回结果 $result = [ 'project' => $name, 'overall_status' => 'success', 'message' => '检查通过', 'checks' => [], ]; // 定义检查项数组 $checks = []; try { // 1. 核心目录存在性检查(前端目录统一在 frontend 下) $coreDirectories = [ 'admin' => [ 'path' => $this->getFrontendProjectPath('admin') . DIRECTORY_SEPARATOR, 'name' => 'Admin目录', ], 'web' => [ 'path' => $this->getFrontendProjectPath('web') . DIRECTORY_SEPARATOR, 'name' => 'Web目录', ], ]; foreach ($coreDirectories as $dirKey => $dirInfo) { if (is_dir($dirInfo['path'])) { $checks[] = [ 'type' => 'directory', 'name' => $dirInfo['name'], 'path' => $dirInfo['path'], 'status' => 'success', 'message' => "{$dirInfo['name']}存在", 'exception' => null, ]; } else { $checks[] = [ 'type' => 'directory', 'name' => $dirInfo['name'], 'path' => $dirInfo['path'], 'status' => 'error', 'message' => "{$dirInfo['name']}不存在", 'exception' => [ 'type' => 'DirectoryNotFoundException', 'message' => __('common.plugin.error.dir_not_exist', ['name' => $dirKey]), ], ]; } } // 2. 插件安装目录权限检查(前端目录统一在 frontend 下) $adminPluginDirectory = $this->getFrontendProjectPath('admin') . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'apps' . DIRECTORY_SEPARATOR; $webPluginDirectory = $this->getFrontendProjectPath('web') . DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'apps' . DIRECTORY_SEPARATOR; $resourceDirectory = public_path() . DIRECTORY_SEPARATOR; $permissionChecks = [ [ 'name' => '插件源码目录', 'path' => $this->plugin_path, 'permission_type' => 'readable', 'description' => '插件源码目录可读性检查', ], [ 'name' => 'Admin插件目录', 'path' => $adminPluginDirectory, 'permission_type' => 'writable', 'description' => '插件将安装到的Admin目录可写性检查', ], [ 'name' => 'Web插件目录', 'path' => $webPluginDirectory, 'permission_type' => 'writable', 'description' => '插件将安装到的Web目录可写性检查', ], [ 'name' => '公共资源目录', 'path' => $resourceDirectory, 'permission_type' => 'writable', 'description' => '插件资源将安装到的公共目录可写性检查', ], ]; foreach ($permissionChecks as $checkItem) { $path = $checkItem['path']; $checkType = $checkItem['permission_type']; $checkName = $checkItem['name']; if ($checkType === 'readable') { $status = is_readable($path) ? 'success' : 'error'; $message = $status === 'success' ? "{$checkName}可读" : "{$checkName}不可读"; } else { // 可写检查:目录存在则检查是否可写,不存在则尝试创建 if (is_dir($path)) { $status = is_write($path) ? 'success' : 'error'; $message = $status === 'success' ? "{$checkName}可写" : "{$checkName}不可写"; } else { $status = mkdir($path, 0777, true) ? 'success' : 'error'; $message = $status === 'success' ? "{$checkName}创建成功并可写" : "{$checkName}创建失败或不可写"; } } $checks[] = [ 'type' => 'permission', 'name' => $checkName, 'path' => $path, 'permission_type' => $checkType, 'description' => $checkItem['description'], 'status' => $status, 'message' => $message, 'exception' => $status === 'error' ? [ 'type' => 'PermissionException', 'message' => $message, ] : null, ]; } // 4. 插件信息检查 $pluginConfig = $this->getPluginConfig($name); if (empty($pluginConfig)) { $checks[] = [ 'type' => 'plugin_info', 'name' => '插件信息检查', 'description' => '检查插件的info.php文件是否存在', 'status' => 'error', 'message' => "插件信息文件不存在", 'exception' => [ 'type' => 'InfoFileNotFoundException', 'message' => __('common.plugin.error.info_file_not_exist', ['name' => $name]), ], ]; } else { $checks[] = [ 'type' => 'plugin_info', 'name' => '插件信息检查', 'description' => '验证插件信息文件的完整性', 'status' => 'success', 'message' => "插件信息文件存在", 'plugin_info' => [ 'code' => $pluginConfig['code']??'', 'name' => $pluginConfig['name']??'', 'description' => $pluginConfig['description'] ?? '', 'version' => $pluginConfig['version'], ], 'exception' => null, ]; } // 3. 插件安装状态检查(检查 installed.php 文件) $pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $name; $installedConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php'; if (is_file($installedConfigFile)) { $checks[] = [ 'type' => 'installation_status', 'name' => '插件安装状态检查', 'description' => '检查插件是否已安装', 'status' => 'error', 'message' => "插件已安装", 'exception' => [ 'type' => 'PluginAlreadyInstalledException', 'message' => __('common.plugin.error.repeat_install', ['name' => $name]), ], ]; } else { $checks[] = [ 'type' => 'installation_status', 'name' => '插件安装状态检查', 'description' => '确认插件未安装', 'status' => 'success', 'message' => "插件未安装,可以继续", 'exception' => null, ]; } // 4. 插件依赖检查(检查 installed.php 文件) if (isset($pluginConfig['support_app']) && !empty($pluginConfig['support_app']) && $pluginConfig['support_app'] != $name) { $supportAppCode = $pluginConfig['support_app']; $supportAppDir = $this->plugin_path . DIRECTORY_SEPARATOR . $supportAppCode; $supportAppInstalledFile = $supportAppDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php'; if (!is_file($supportAppInstalledFile)) { $supportAppConfig = $this->getPluginConfig($supportAppCode); $supportAppTitle = empty($supportAppConfig) ? $supportAppCode : $supportAppConfig['title']; $checks[] = [ 'type' => 'dependency', 'name' => '插件依赖检查', 'description' => '检查插件依赖的主应用是否已安装', 'dependency_name' => $supportAppCode, 'dependency_title' => $supportAppTitle, 'status' => 'error', 'message' => "依赖插件未安装", 'exception' => [ 'type' => 'DependencyNotFoundException', 'message' => $pluginConfig['title'] . '插件的主应用' . $supportAppTitle . '插件还未安装,请先安装主应用', ], ]; } else { $checks[] = [ 'type' => 'dependency', 'name' => '插件依赖检查', 'description' => '确认插件依赖已满足', 'dependency_name' => $supportAppCode, 'status' => 'success', 'message' => "依赖插件已安装", 'exception' => null, ]; } } // 5. 框架版本支持检查 $currentFrameworkVersion = config('config.app.version', 'v1.0.1'); if (isset($pluginConfig['support_version']) && !empty($pluginConfig['support_version'])) { $checks[] = [ 'type' => 'version', 'name' => '框架版本支持检查', 'description' => '验证当前框架版本是否支持该插件', 'current_version' => $currentFrameworkVersion, 'required_version' => $pluginConfig['support_version'], 'status' => 'success', 'message' => "当前框架版本支持该插件", 'exception' => null, ]; } } catch (\Exception $e) { // 捕获异常 $checks[] = [ 'type' => 'system', 'name' => '系统检查', 'description' => '系统级检查', 'status' => 'error', 'message' => $e->getMessage(), 'exception' => [ 'type' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine(), ], ]; } // 计算整体状态 $errorCount = 0; $warningCount = 0; foreach ($checks as $check) { if ($check['status'] === 'error') { $errorCount++; } elseif ($check['status'] === 'warning') { $warningCount++; } } if ($errorCount > 0) { $result['overall_status'] = 'error'; $result['message'] = "检查失败,发现{$errorCount}个错误"; } elseif ($warningCount > 0) { $result['overall_status'] = 'warning'; $result['message'] = "检查通过但有{$warningCount}个警告"; } else { $result['overall_status'] = 'success'; $result['message'] = "所有检查通过"; } // 添加检查项到结果 $result['checks'] = $checks; return $result; } }