diff --git a/src/Templates/PluginMain.php.template b/src/Templates/PluginMain.php.template
index 3787e18e..a0607351 100644
--- a/src/Templates/PluginMain.php.template
+++ b/src/Templates/PluginMain.php.template
@@ -4,10 +4,13 @@ declare(strict_types=1);
namespace {namespace};
+use ZM\Annotation\OneBot\BotCommand;
+use ZM\Context\BotContext;
+
class {class}
{
- #[\BotCommand(match: '测试{basename}')]
- public function firstBotCommand(\BotContext $ctx): void
+ #[BotCommand(match: '测试{basename}')]
+ public function firstBotCommand(BotContext $ctx): void
{
$ctx->reply('这是{name}插件的第一个命令!');
}
diff --git a/src/ZM/Command/Plugin/PluginCommand.php b/src/ZM/Command/Plugin/PluginCommand.php
index 94b18812..8618aa4f 100644
--- a/src/ZM/Command/Plugin/PluginCommand.php
+++ b/src/ZM/Command/Plugin/PluginCommand.php
@@ -125,9 +125,9 @@ abstract class PluginCommand extends Command
{
return match ($type) {
ZM_PLUGIN_TYPE_NATIVE => '内部',
- ZM_PLUGIN_TYPE_PHAR => 'Phar',
- ZM_PLUGIN_TYPE_SOURCE => '源码',
- ZM_PLUGIN_TYPE_COMPOSER => 'Composer 外部加载',
+ ZM_PLUGIN_TYPE_PHAR => 'Phar',
+ ZM_PLUGIN_TYPE_SOURCE => '源码>',
+ ZM_PLUGIN_TYPE_COMPOSER => 'Composer',
default => '未知模式'
};
}
diff --git a/src/ZM/Command/Plugin/PluginInstallCommand.php b/src/ZM/Command/Plugin/PluginInstallCommand.php
index c491a517..9e8069a1 100644
--- a/src/ZM/Command/Plugin/PluginInstallCommand.php
+++ b/src/ZM/Command/Plugin/PluginInstallCommand.php
@@ -7,10 +7,9 @@ namespace ZM\Command\Plugin;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
-use ZM\Exception\FileSystemException;
-use ZM\Plugin\PluginManager;
+use ZM\Plugin\Strategy\ComposerStrategy;
+use ZM\Plugin\Strategy\GitStrategy;
use ZM\Store\FileSystem;
-use ZM\Utils\ZMRequest;
#[AsCommand(name: 'plugin:install', description: '从 GitHub 或其他 Git 源码托管站安装插件')]
class PluginInstallCommand extends PluginCommand
@@ -18,6 +17,7 @@ class PluginInstallCommand extends PluginCommand
protected function configure()
{
$this->addArgument('address', InputArgument::REQUIRED, '插件地址');
+ $this->addOption('github-token', null, InputOption::VALUE_REQUIRED, '提供的 GitHub Token');
// 下面是辅助用的,和 server:start 一样
$this->addOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录');
@@ -29,87 +29,36 @@ class PluginInstallCommand extends PluginCommand
protected function handle(): int
{
$addr = $this->input->getArgument('address');
- $name = ob_uuidgen();
$this->plugin_dir = FileSystem::isRelativePath(config('global.plugin.load_dir', 'plugins')) ? (WORKING_DIR . '/' . config('global.plugin.load_dir', 'plugins')) : config('global.plugin.load_dir', 'plugins');
- // 先通过 GitHub API 获取看看存不存在 zmplugin.json
- // 解析 git https 路径中的仓库所有者和仓库名
- $git_url = parse_url($addr);
- if ($git_url['host'] === 'github.com') {
- $path = explode('/', $git_url['path']);
- $owner = $path[1];
- $repo = $path[2];
- if (str_ends_with($repo, '.git')) {
- $repo = substr($repo, 0, -4);
- }
- $api = ZMRequest::get('https://api.github.com/repos/' . $owner . '/' . $repo . '/contents/zmplugin.json', ['User-Agent' => 'ZMFramework']);
- if ($api === false) {
- $this->error('GitHub API 请求失败');
- return static::FAILURE;
- }
- $api = json_decode($api, true);
- if (isset($api['message'])) {
- $this->error('该项目中不存在 zmplugin.json 元信息!');
- return static::FAILURE;
- }
- $contents = implode('', array_map(fn ($x) => base64_decode($x), explode("\n", $api['content'])));
- $json = json_decode($contents, true);
- if (!isset($json['name'])) {
- $this->error('插件元信息内没有名字!');
- return static::FAILURE;
- }
- $plugin_name = $json['name'];
- if (PluginManager::isPluginExists($plugin_name)) {
- $this->error('插件 ' . $plugin_name . ' 已存在,无法再次安装!');
- return static::FAILURE;
- }
+
+ // 先检查传入的参数是什么类型。如果是 a/b 类型则为 composer 包
+ if (count(explode('/', $addr)) === 2) {
+ $st = new ComposerStrategy($addr, $this->plugin_dir, logger: $this);
+ } elseif ((parse_url($addr)['scheme'] ?? null) !== null) {
+ $st = new GitStrategy($addr, $this->plugin_dir, logger: $this);
+ } else {
+ $this->error('无法检测输入要安装插件的链接或名字,请检查后再试!');
+ return static::FAILURE;
}
- $this->info('正在从 ' . $addr . ' 克隆插件仓库');
+
+ if ($token = $this->input->getOption('github-token')) {
+ $option = ['github-token' => $token];
+ } else {
+ $option = [];
+ }
+ // 然后调用安装并看是否成功
try {
- FileSystem::createDir($this->plugin_dir);
- } catch (FileSystemException $exception) {
- $this->error("无法创建插件目录 {$this->plugin_dir}:{$exception->getMessage()}");
- return static::FAILURE;
- }
- passthru('cd ' . escapeshellarg($this->plugin_dir) . ' && git clone --depth=1 ' . escapeshellarg($addr) . ' ' . $name, $code);
- if ($code !== 0) {
- $this->error('无法从指定 Git 地址拉取项目,请检查地址名是否正确');
- return static::FAILURE;
- }
- if (!file_exists($this->plugin_dir . '/' . $name . '/zmplugin.json')) {
- $this->error('项目不存在 zmplugin.json 插件元信息,无法安装,请手动删除目录 ' . $name);
- // TODO: 使用 rmdir 和 unlink 删除 git 目录
- return static::FAILURE;
- }
- $this->output->writeln('正在检查元信息完整性');
- $getname = json_decode(file_get_contents($this->plugin_dir . '/' . $name . '/zmplugin.json'), true)['name'] ?? null;
- if ($getname === null) {
- $this->error('无法获取元信息 zmplugin.json');
- return static::FAILURE;
- }
- $code = rename($this->plugin_dir . '/' . $name, $this->plugin_dir . '/' . $getname);
- if ($code === false) {
- $this->error('无法重命名文件夹 ' . $name);
- return static::FAILURE;
- }
- if (file_exists($this->plugin_dir . '/' . $getname . '/composer.json')) {
- $this->info('插件存在 composer.json,正在安装 composer 相关依赖(需要系统环境变量中包含 composer 路径)');
- $cwd = getcwd();
- chdir($this->plugin_dir . '/' . $getname);
- // 使用内建 Composer
- if (file_exists(WORKING_DIR . '/runtime/composer.phar')) {
- $this->info('使用内建 Composer');
- passthru(PHP_BINARY . ' ' . escapeshellarg(WORKING_DIR . '/runtime/composer.phar') . ' install --no-dev', $code);
- } else {
- $this->info('使用系统 Composer');
- passthru('composer install --no-dev', $code);
- }
- chdir($cwd);
- if ($code != 0) {
- $this->error('无法安装 Composer 依赖,请检查 Composer 是否可以正常运行');
+ if (!$st->install($option)) {
+ $this->error('插件安装失败,' . $st->getError());
return static::FAILURE;
}
+ } catch (\Throwable $e) {
+ $this->error('niu: ' . $e->getMessage());
+ $this->error($e->getTraceAsString());
+ return static::FAILURE;
}
- $this->info('插件 ' . $getname . ' 安装成功!');
+
+ $this->info('插件 ' . $st->getInstalledName() . ' 安装成功!');
return static::SUCCESS;
}
}
diff --git a/src/ZM/Command/Plugin/PluginListCommand.php b/src/ZM/Command/Plugin/PluginListCommand.php
index 9ecdfd90..e982a58b 100644
--- a/src/ZM/Command/Plugin/PluginListCommand.php
+++ b/src/ZM/Command/Plugin/PluginListCommand.php
@@ -11,13 +11,30 @@ use ZM\Plugin\PluginManager;
#[AsCommand(name: 'plugin:list', description: '显示插件列表')]
class PluginListCommand extends PluginCommand
{
+ protected function configure()
+ {
+ $this->addOption('name-list', 'N', null, '只输出插件列表的名字');
+ }
+
protected function handle(): int
{
$all = PluginManager::getPlugins();
+ if ($all === []) {
+ $this->info('当前未安装任何插件');
+ return static::SUCCESS;
+ }
+ if ($this->input->getOption('name-list')) {
+ $this->info('插件列表: ');
+ foreach ($all as $k => $v) {
+ $this->write($k);
+ }
+ return static::SUCCESS;
+ }
$table = new Table($this->output);
- $table->setHeaders(['名称', '版本', '类型']);
+ $table->setColumnMaxWidth(2, 27);
+ $table->setHeaders(['名称', '版本', '简介', '类型']);
foreach ($all as $k => $v) {
- $table->addRow([$k, $v->getVersion(), $this->getTypeDisplayName($v->getPluginType())]);
+ $table->addRow([$k, $v->getVersion(), $v->getDescription(), $this->getTypeDisplayName($v->getPluginType())]);
}
$table->setStyle('box');
$table->render();
diff --git a/src/ZM/Plugin/PluginManager.php b/src/ZM/Plugin/PluginManager.php
index 5521252d..a3689e06 100644
--- a/src/ZM/Plugin/PluginManager.php
+++ b/src/ZM/Plugin/PluginManager.php
@@ -25,7 +25,7 @@ class PluginManager
}
/**
- * 传入插件父目录,扫描插件目录下的所有插件并注册添加
+ * 传入插件父目录,扫描插件目录下的所有插件并注册添加(开发插件)
*
* @param string $dir 插件目录
* @return int 返回添加插件的数量
@@ -54,21 +54,31 @@ class PluginManager
}
// 先看有没有 zmplugin.json,没有则不是正常的插件,发个 notice 然后跳过
- $meta_file = $item . '/zmplugin.json';
+ $meta_file = $item . '/composer.json';
if (!is_file($meta_file)) {
- logger()->notice('插件目录 {dir} 没有插件元信息(zmplugin.json),跳过扫描。', ['dir' => $item]);
+ logger()->notice('插件目录 {dir} 没有插件元信息(composer.json),跳过扫描。', ['dir' => $item]);
continue;
}
// 检验元信息是否合法,不合法发个 notice 然后跳过
$json_meta = json_decode(file_get_contents($meta_file), true);
if (!is_array($json_meta)) {
- logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
+ logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
+ continue;
+ }
+ if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
+ logger()->notice('插件目录 {dir} 的插件元信息未提供版本和名称,不是有效的插件,跳过扫描。', ['dir' => $item]);
continue;
}
// 构造一个元信息对象
- $meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_SOURCE, $item);
+ $meta = new PluginMeta(
+ name: $json_meta['name'],
+ version: $json_meta['extra']['zm-plugin-version'],
+ description: $json_meta['description'] ?? '',
+ plugin_type: ZM_PLUGIN_TYPE_SOURCE,
+ root_dir: $item
+ );
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
continue;
@@ -93,18 +103,29 @@ class PluginManager
// 加载这个 Phar 文件
$phar = require $phar_path;
// 读取元信息
- $plugin_file_path = zm_dir('phar://' . $phar_path . '/zmplugin.json');
+ $plugin_file_path = zm_dir('phar://' . $phar_path . '/composer.json');
if (!file_exists($plugin_file_path)) {
- throw new PluginException('插件元信息 zmplugin.json 文件不存在');
+ throw new PluginException('插件元信息 composer.json 文件不存在');
}
// 解析元信息的 JSON
- $meta_json = json_decode(file_get_contents($plugin_file_path), true);
+ $json_meta = json_decode(file_get_contents($plugin_file_path), true);
// 失败抛出异常
- if (!is_array($meta_json)) {
+ if (!is_array($json_meta)) {
throw new PluginException('插件信息文件解析失败');
}
+ // 解析 name 和版本失败
+ if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
+ throw new PluginException('插件文件 ' . $phar_path . ' 的插件元信息未提供版本和名称,不是有效的插件');
+ }
// $phar 这时应该是一个 ZMPlugin 对象,写入元信息
- $meta = new PluginMeta($meta_json, ZM_PLUGIN_TYPE_PHAR, zm_dir('phar://' . $phar_path));
+ // 构造一个元信息对象
+ $meta = new PluginMeta(
+ name: $json_meta['name'],
+ version: $json_meta['extra']['zm-plugin-version'],
+ description: $json_meta['description'] ?? '',
+ plugin_type: ZM_PLUGIN_TYPE_PHAR,
+ root_dir: zm_dir('phar://' . $phar_path)
+ );
// 如果已经返回了一个插件对象,那么直接塞进去实体
if ($phar instanceof ZMPlugin) {
$meta->bindEntity($phar);
@@ -144,7 +165,7 @@ class PluginManager
$cnt = 0;
foreach ($json['packages'] as $item) {
$root_dir = $vendor_dir . '/' . $item['name'];
- $meta_file = zm_dir($root_dir . '/zmplugin.json');
+ $meta_file = zm_dir($root_dir . '/composer.json');
if (!file_exists($meta_file)) {
continue;
}
@@ -152,12 +173,22 @@ class PluginManager
// 检验元信息是否合法,不合法发个 notice 然后跳过
$json_meta = json_decode(file_get_contents($meta_file), true);
if (!is_array($json_meta)) {
- logger()->notice('插件目录 {dir} 的插件元信息(zmplugin.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
+ logger()->notice('插件目录 {dir} 的插件元信息(composer.json)不是有效的 JSON,跳过扫描。', ['dir' => $item]);
+ continue;
+ }
+ // 解析 name 和版本失败
+ if (!isset($json_meta['extra']['zm-plugin-version'], $json_meta['name'])) {
continue;
}
// 构造一个元信息对象
- $meta = new PluginMeta($json_meta, ZM_PLUGIN_TYPE_COMPOSER, zm_dir($root_dir));
+ $meta = new PluginMeta(
+ name: $json_meta['name'],
+ version: $json_meta['extra']['zm-plugin-version'],
+ description: $json_meta['description'] ?? '',
+ plugin_type: ZM_PLUGIN_TYPE_COMPOSER,
+ root_dir: $root_dir
+ );
if ($meta->getEntryFile() === null && $meta->getAutoloadFile() === null) {
logger()->notice('插件 ' . $item . ' 不存在入口文件,也没有自动加载文件和内建 Composer,跳过加载');
continue;
@@ -223,6 +254,7 @@ class PluginManager
if ($meta->getPluginType() !== ZM_PLUGIN_TYPE_NATIVE) {
logger()->info('正在启用插件 ' . $name);
}
+ /* 插件从 zmplugin.json 改为 composer 了,所以不需要自己判断依赖
// 先判断依赖关系,如果声明了依赖,但依赖不合规则报错崩溃
foreach ($meta->getDependencies() as $dep_name => $dep_version) {
// 缺少依赖的插件,不行
@@ -233,7 +265,7 @@ class PluginManager
if (VersionComparator::compareVersionRange(self::$plugins[$dep_name]->getVersion(), $dep_version) === false) {
throw new PluginException('插件 ' . $name . ' 依赖插件 ' . $dep_name . ',但是这个插件的版本不符合要求');
}
- }
+ }*/
// 如果插件为单文件形式,且设置了 pluginLoad 事件,那就调用
$meta->getEntity()?->emitPluginLoad($parser);
if (($entity = $meta->getEntity()) instanceof ZMPlugin) {
diff --git a/src/ZM/Plugin/PluginMeta.php b/src/ZM/Plugin/PluginMeta.php
index 1fe7d241..ce88e95c 100644
--- a/src/ZM/Plugin/PluginMeta.php
+++ b/src/ZM/Plugin/PluginMeta.php
@@ -20,41 +20,22 @@ class PluginMeta implements \JsonSerializable
/** @var string 插件描述 */
private string $description;
- /** @var array 插件的依赖列表 */
- private array $dependencies;
-
/** @var null|string 插件的根目录 */
private ?string $root_dir;
/** @var int 插件类型 */
private int $plugin_type;
- /** @var array 元信息原文 */
- private array $metas;
-
private bool $enabled = true;
private ?ZMPlugin $entity = null;
- /**
- * @param array $meta 元信息数组格式
- * @param int $plugin_type 插件类型
- * @param null|string $root_dir 插件根目录
- */
- public function __construct(array $meta, int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null)
+ public function __construct(string $name, string $version = '1.0-dev', string $description = '', int $plugin_type = ZM_PLUGIN_TYPE_NATIVE, ?string $root_dir = null)
{
- // 设置名称
- $this->name = $meta['name'] ?? '';
- // 设置版本
- $this->version = $meta['version'] ?? '1.0-dev';
- // 设置描述
- $this->description = $meta['description'] ?? '';
- // 设置依赖
- $this->dependencies = $meta['dependencies'] ?? [];
- $this->metas = $meta;
- // 设置插件根目录
+ $this->name = $name;
+ $this->version = $version;
+ $this->description = $description;
$this->plugin_type = $plugin_type;
- // 设置插件根目录
$this->root_dir = $root_dir;
}
@@ -148,11 +129,6 @@ class PluginMeta implements \JsonSerializable
return $this->description;
}
- public function getDependencies(): array
- {
- return $this->dependencies;
- }
-
public function getRootDir(): string
{
return $this->root_dir;
@@ -174,15 +150,9 @@ class PluginMeta implements \JsonSerializable
'name' => $this->name,
'version' => $this->version,
'description' => $this->description,
- 'dependencies' => $this->dependencies,
];
}
- public function getMetas(): array
- {
- return $this->metas;
- }
-
public function getEntity(): ?ZMPlugin
{
return $this->entity;
diff --git a/src/ZM/Plugin/Strategy/ComposerStrategy.php b/src/ZM/Plugin/Strategy/ComposerStrategy.php
new file mode 100644
index 00000000..b59721f2
--- /dev/null
+++ b/src/ZM/Plugin/Strategy/ComposerStrategy.php
@@ -0,0 +1,15 @@
+error = 'Not implemented';
+ return false;
+ }
+}
diff --git a/src/ZM/Plugin/Strategy/GitStrategy.php b/src/ZM/Plugin/Strategy/GitStrategy.php
new file mode 100644
index 00000000..cca5d949
--- /dev/null
+++ b/src/ZM/Plugin/Strategy/GitStrategy.php
@@ -0,0 +1,149 @@
+git_api_link = $option['git-api-link'];
+ }
+ if (isset($option['github-token'])) {
+ $this->token = $option['github-token'];
+ }
+
+ $git_url = parse_url($this->input);
+
+ // GitHub 做特殊处理,直接调用 API 检查
+ $is_github = false;
+ $plugin_name = null;
+ if ($git_url['host'] === 'github.com' && ($option['github-skip-check'] ?? false) !== true) {
+ if (!$this->checkGitAPI($git_url, $plugin_name)) {
+ return false;
+ }
+ $is_github = true;
+ }
+
+ // 使用 Composer 管理插件,将仓库链接绑定到 composer.json
+ $this->logger->info('正在使用 Composer 下载并安装插件');
+ $composer = ZMUtil::getComposerMetadata($this->root_composer_path);
+ $origin_composer = $composer;
+ $already_has_repo = false;
+ // 不破坏原有队列,加入 GitHub 的 repo
+ if (!isset($composer['repositories'])) {
+ $composer['repositories'] = [];
+ }
+ if (is_assoc_array($composer['repositories'])) {
+ $composer['repositories'] = [$composer['repositories']];
+ }
+ foreach ($composer['repositories'] as $v) {
+ if (($v['url'] ?? '') === $this->input) {
+ $already_has_repo = true;
+ break;
+ }
+ }
+ if (!$already_has_repo) {
+ $composer['repositories'][] = [
+ 'type' => $is_github ? 'github' : 'git',
+ 'url' => $this->input,
+ '.belongs' => $plugin_name,
+ ];
+ }
+ // 写入 composer.json
+ if (ZMUtil::putComposerMetadata($this->root_composer_path, $composer) === false) {
+ $this->error = '写入 composer.json 失败';
+ return false;
+ }
+ $env = getenv('COMPOSER_EXECUTABLE');
+ if ($env === false) {
+ $env = 'composer';
+ }
+ if ($plugin_name === null) {
+ $this->error = '没有从 Git 获取到插件的元信息,目前无法从 GitHub 以外的 Git 仓库下载插件,后续会更新!';
+ ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
+ return false;
+ }
+ if ($plugin_name === '') {
+ $this->error = '获取插件名称失败!';
+ ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
+ return false;
+ }
+ if (function_exists('pcntl_signal')) {
+ pcntl_signal(SIGINT, function () use ($origin_composer) {
+ echo "强行中断,恢复 Composer 中\n";
+ ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
+ });
+ }
+ passthru("{$env} require {$plugin_name}", $code);
+ if (function_exists('pcntl_signal')) {
+ pcntl_signal(SIGINT, SIG_IGN);
+ }
+ if ($code !== 0) {
+ $this->error = '使用 composer 引入 Git 插件出现了一些错误,请看上方错误';
+ ZMUtil::putComposerMetadata($this->root_composer_path, $origin_composer);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * 用于调用 GitHub 的 API 用作不下载就检查插件是否合规
+ *
+ * @param array $git_url 解析后的链接
+ */
+ private function checkGitAPI(array $git_url, ?string &$plugin_name = null): bool
+ {
+ $this->logger->info('正在检查 GitHub 插件是否为框架插件');
+ [, $owner, $repo] = explode('/', $git_url['path']);
+ if (str_ends_with($repo, '.git')) {
+ $repo = substr($repo, 0, -4);
+ }
+
+ // 调用 HTTP 客户端获取 API 信息
+ $header = ['User-Agent' => 'zhamao-framework'];
+ if ($this->token !== '') {
+ $header['Authorization'] = 'token ' . $this->token;
+ }
+ $api = ZMRequest::get(
+ str_replace(['{owner}', '{repo}'], [$owner, $repo], $this->git_api_link),
+ $header,
+ only_body: false
+ );
+ if ($api->getStatusCode() !== 200) {
+ $this->error = "GitHub API 请求失败[{$api->getStatusCode()}]";
+ if ($api->getStatusCode() === 403) {
+ $this->error .= '可能是 API 滥用导致的,建议生成一个 GitHub Token。';
+ }
+ return false;
+ }
+
+ // 检查插件的 composer.json 是否合规
+ $content = json_decode($api->getBody()->getContents(), true);
+ if (isset($content['message'])) {
+ $this->error = '该 GitHub 仓库中不存在 composer.json 文件!';
+ return false;
+ }
+ $contents = implode('', array_map(fn ($x) => base64_decode($x), explode("\n", $content['content'])));
+ $json = json_decode($contents, true);
+ if (!$this->checkComposerIntegrity($json)) {
+ return false;
+ }
+ $plugin_name = $json['name'];
+ $this->installed_name = $plugin_name;
+ return true;
+ }
+}
diff --git a/src/ZM/Plugin/Strategy/PluginInstallStrategy.php b/src/ZM/Plugin/Strategy/PluginInstallStrategy.php
new file mode 100644
index 00000000..edc23a92
--- /dev/null
+++ b/src/ZM/Plugin/Strategy/PluginInstallStrategy.php
@@ -0,0 +1,67 @@
+root_composer_path === '') {
+ $this->root_composer_path = zm_dir(WORKING_DIR);
+ }
+ if ($this->logger === null) {
+ $this->logger = ob_logger();
+ }
+ }
+
+ abstract public function install(array $option = []): bool;
+
+ public function getError(): string
+ {
+ return $this->error;
+ }
+
+ public function getInstalledName(): string
+ {
+ return $this->installed_name;
+ }
+
+ /**
+ * 用于检查 Composer 文件的信息是否完整
+ */
+ protected function checkComposerIntegrity(mixed $composer): bool
+ {
+ // 必须是 array
+ if (!is_array($composer)) {
+ $this->error = 'composer.json 元信息获取出错';
+ return false;
+ }
+ if (!isset($composer['extra']['zm-plugin-version'])) {
+ $this->error = 'composer.json 内没有标明该炸毛插件的版本,或该仓库不是炸毛插件';
+ return false;
+ }
+ if (!isset($composer['name'])) {
+ $this->error = 'composer.json 插件元信息内没有名字!';
+ return false;
+ }
+ $plugin_name = $composer['name'];
+ if (PluginManager::isPluginExists($plugin_name)) {
+ $this->error = "插件 {$plugin_name} 已存在,无法再次安装";
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/ZM/ZMApplication.php b/src/ZM/ZMApplication.php
index 15f5c6a3..a1ed6bba 100644
--- a/src/ZM/ZMApplication.php
+++ b/src/ZM/ZMApplication.php
@@ -50,7 +50,7 @@ class ZMApplication extends ZMPlugin
*/
public function run()
{
- $meta = new PluginMeta(['name' => 'native'], ZM_PLUGIN_TYPE_NATIVE);
+ $meta = new PluginMeta(name: 'native', plugin_type: ZM_PLUGIN_TYPE_NATIVE);
$meta->bindEntity($this);
PluginManager::addPlugin($meta);
(new Framework($this->args))->init()->start();