input('uuid', $sessionUuid); } // 检查插件实体是否存在 $pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $code; $pluginConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php'; if (!is_dir($pluginDir) || !is_file($pluginConfigFile)) { yield Sse::error('插件不存在,无法卸载', [], $sessionUuid); return; } // 检查插件是否允许卸载 $pluginConfig = $this->getPluginConfig($code); if (isset($pluginConfig['uninstall']['undeletable']) && $pluginConfig['uninstall']['undeletable'] === true) { yield Sse::error('该插件为系统内置插件,不允许卸载', [], $sessionUuid); return; } $installedConfigPath = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php'; if (!is_file($installedConfigPath)) { yield Sse::error('插件已卸载,无需重复执行卸载操作', [], $sessionUuid); return; } // 检查插件是否提供 Install.php $className = "plugin\\{$code}\\Install"; if (!class_exists($className)) { yield Sse::error("插件未提供 Install.php,无法进行卸载", [], $sessionUuid); return; } try { // 发送卸载开始事件 yield Sse::progress('开始卸载插件', 10, ['plugin' => $code], $sessionUuid); // 统一调用插件的 Install::uninstall() 方法(这是唯一的卸载入口) yield Sse::progress('执行插件卸载逻辑', 30, [], $sessionUuid); $version = $pluginConfig['version'] ?? '1.0.0'; // 实例化并执行插件 Install 类(传入进度回调以支持在线模式反馈) $installInstance = new $className(); // 创建临时输出文件(类似 Terminal 的方式) $outputFile = runtime_path(self::RUNTIME_PLUGIN_PATH . '/uninstall_' . $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::uninstall() 内部的 echo 输出 ob_start(); $installInstance->uninstall($version); $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)) { // 使用 Sse::progress 统一输出,不更新进度值 yield Sse::progress($line, 30, [], $sessionUuid); } } } // 清理临时文件 @unlink($outputFile); yield Sse::progress('插件卸载方法执行成功', 80, [], $sessionUuid); // 删除插件内的安装配置 yield Sse::progress('删除安装配置', 90, [], $sessionUuid); if (is_file($installedConfigPath)) { @unlink($installedConfigPath); } // 发送卸载完成事件 yield Sse::completed('插件卸载成功', ['plugin' => $code], $sessionUuid); } catch (\Throwable $e) { // 发送卸载失败事件 yield Sse::error('卸载失败:' . $e->getMessage(), [ 'exception' => get_class($e), 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), ], $sessionUuid); } } /** * 删除插件(仅删除插件包) * 删除条件: * 1. 插件必须为已卸载状态(installed.php 不存在) * 2. 非官方插件(undeletable != true) * * @param string $code 插件编码 * * @return void 删除成功返回 true * @throws \Exception */ public function delete(string $code): void { // 1. 检查插件实体是否存在 $pluginDir = $this->plugin_path . DIRECTORY_SEPARATOR . $code; $pluginConfigFile = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'info.php'; if (!is_dir($pluginDir) || !is_file($pluginConfigFile)) { throw new \Exception('插件不存在,无法删除'); } // 2. 检查插件配置 $pluginConfig = $this->getPluginConfig($code); // 3. 检查是否为系统内置插件(不允许删除) if (isset($pluginConfig['uninstall']['undeletable']) && $pluginConfig['uninstall']['undeletable'] === true) { throw new \Exception('系统内置插件不允许删除'); } // 4. 检查插件安装状态(installed.php 不存在) $installedConfigPath = $pluginDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'installed.php'; $isInstalled = is_file($installedConfigPath); if ($isInstalled) { throw new \Exception('插件已安装,请先卸载后再删除'); } // 5. 检查插件是否提供 Install.php $className = "plugin\\{$code}\\Install"; if (!class_exists($className)) { throw new \Exception("插件未提供 Install.php,无法进行删除"); } try { $version = $pluginConfig['version'] ?? '1.0.0'; // 实例化并执行插件 Install 类 $installInstance = new $className(); // 设置为 CLI 模式(不使用回调) $installInstance->setOnlineMode(false); // 执行删除 $installInstance->delete($version); // 删除后,需要清理数据库中的插件记录 $this->deletePluginRecord($code); } catch (\Throwable $e) { throw new \Exception('删除失败:' . $e->getMessage()); } } /** * 删除插件数据库记录 */ protected function deletePluginRecord(string $code): void { try { /** @var PluginService $service */ $service = Container::make(PluginService::class); $model = $service->dao->query()->where('key', $code)->first(); if ($model) { $model->delete(); } } catch (\Throwable $e) { throw new \RuntimeException('删除插件记录失败:' . $e->getMessage(), 0, $e); } } }