diff --git a/src/Templates/PluginMain.php.template b/src/Templates/PluginMain.php.template
new file mode 100644
index 00000000..b845d056
--- /dev/null
+++ b/src/Templates/PluginMain.php.template
@@ -0,0 +1,14 @@
+reply('这是{name}插件的第一个命令!');
+ }
+}
diff --git a/src/Templates/main.php.template b/src/Templates/main.php.template
new file mode 100644
index 00000000..693ca86c
--- /dev/null
+++ b/src/Templates/main.php.template
@@ -0,0 +1,14 @@
+on(fn () => '这是{name}插件的第一个命令!');
+
+$plugin->addBotCommand($cmd1);
+
+return $plugin;
diff --git a/src/ZM/Command/Command.php b/src/ZM/Command/Command.php
index 97893708..ebb87910 100644
--- a/src/ZM/Command/Command.php
+++ b/src/ZM/Command/Command.php
@@ -33,6 +33,11 @@ abstract class Command extends \Symfony\Component\Console\Command\Command
$this->input = $input;
$this->output = $output;
if ($this->shouldExecute()) {
+ if (property_exists($this, 'bootstrappers')) {
+ foreach ($this->bootstrappers as $bootstrapper) {
+ (new $bootstrapper())->bootstrap($this->input->getOptions());
+ }
+ }
return $this->handle();
}
return self::SUCCESS;
diff --git a/src/ZM/Command/Plugin/PluginMakeCommand.php b/src/ZM/Command/Plugin/PluginMakeCommand.php
new file mode 100644
index 00000000..d55dbfc3
--- /dev/null
+++ b/src/ZM/Command/Plugin/PluginMakeCommand.php
@@ -0,0 +1,123 @@
+addArgument('name', InputArgument::OPTIONAL, '插件名称', null);
+ $this->addOption('author', 'a', InputOption::VALUE_OPTIONAL, '作者名称', null);
+ $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, '插件描述', null);
+ $this->addOption('plugin-version', null, InputOption::VALUE_OPTIONAL, '插件版本', '1.0.0');
+ $this->addOption('type', 'T', InputOption::VALUE_OPTIONAL, '插件类型', null);
+
+ // 下面是 type=psr4 的选项
+ $this->addOption('namespace', null, InputOption::VALUE_OPTIONAL, '插件命名空间', null);
+
+ // 下面是辅助用的,和 server:start 一样
+ $this->addOption('config-dir', null, InputOption::VALUE_REQUIRED, '指定其他配置文件目录');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function handle(): int
+ {
+ $load_dir = config('global.plugin.load_dir');
+ if (empty($load_dir)) {
+ $load_dir = SOURCE_ROOT_DIR . '/plugins';
+ } elseif (FileSystem::isRelativePath($load_dir)) {
+ $load_dir = SOURCE_ROOT_DIR . '/' . $load_dir;
+ }
+ $plugin_dir = zm_dir($load_dir);
+ /** @var QuestionHelper $helper */
+ $helper = $this->getHelper('question');
+
+ $other_plugins = is_dir($plugin_dir) ? FileSystem::scanDirFiles($plugin_dir, false, true, true) : [];
+
+ // 询问插件名称
+ if ($this->input->getArgument('name') === null) {
+ $question = new Question('请输入插件名称:');
+ $question->setValidator(function ($answer) use ($plugin_dir, $other_plugins) {
+ if (empty($answer)) {
+ throw new \RuntimeException('插件名称不能为空');
+ }
+ if (is_numeric(mb_substr($answer, 0, 1))) {
+ throw new \RuntimeException('插件名称不能以数字开头,且只能包含字母、数字、下划线、短横线');
+ }
+ if (!preg_match('/^[a-zA-Z0-9_-]+$/', $answer)) {
+ throw new \RuntimeException('插件名称只能包含字母、数字、下划线、短横线');
+ }
+ if (is_dir($plugin_dir . '/' . strtolower($answer))) {
+ throw new \RuntimeException('插件目录已存在,请换个名字');
+ }
+ foreach ($other_plugins as $dir_name) {
+ $plugin_name = file_exists($plugin_dir . '/' . $dir_name . '/zmplugin.json') ? (json_decode(file_get_contents($plugin_dir . '/' . $dir_name . '/zmplugin.json'), true)['name'] ?? null) : null;
+ if ($plugin_name !== null && $plugin_name === $answer) {
+ throw new \RuntimeException('插件名称已存在,请换个名字');
+ }
+ }
+ return $answer;
+ });
+ $this->input->setArgument('name', $helper->ask($this->input, $this->output, $question));
+ }
+
+ // 询问插件类型
+ if ($this->input->getOption('type') === null) {
+ $question = new ChoiceQuestion(
+ '请输入要生成的插件结构类型',
+ ['file' => 'file 类型为单文件,方便写简单功能', 'psr4' => 'psr4 类型为目录,按照 psr-4 结构生成,同时将生成 composer.json 用来支持自动加载']
+ );
+ $this->input->setOption('type', $helper->ask($this->input, $this->output, $question));
+ }
+
+ if ($this->input->getOption('type') === 'psr4') {
+ // 询问命名空间
+ if ($this->input->getOption('namespace') === null) {
+ $question = new Question('请输入插件命名空间:');
+ $question->setValidator(function ($answer) {
+ if (empty($answer)) {
+ throw new \RuntimeException('插件命名空间不能为空');
+ }
+ if (is_numeric(mb_substr($answer, 0, 1))) {
+ throw new \RuntimeException('插件命名空间不能以数字开头,且只能包含字母、数字、反斜线');
+ }
+ // 只能包含字母、数字和反斜线
+ if (!preg_match('/^[a-zA-Z0-9\\\\]+$/', $answer)) {
+ throw new \RuntimeException('插件命名空间只能包含字母、数字、反斜线');
+ }
+ return $answer;
+ });
+ $this->input->setOption('namespace', $helper->ask($this->input, $this->output, $question));
+ }
+ }
+
+ $generator = new PluginGenerator($this->input->getArgument('name'), $plugin_dir);
+ $generator->generate($this->input->getOptions());
+
+ $this->info('已生成插件:' . $this->input->getArgument('name'));
+ $this->info('目录位置:' . zm_dir($plugin_dir . '/' . $this->input->getArgument('name')));
+ return self::SUCCESS;
+ }
+}
diff --git a/src/ZM/Store/FileSystem.php b/src/ZM/Store/FileSystem.php
index 685f18a3..8ce7e3e7 100644
--- a/src/ZM/Store/FileSystem.php
+++ b/src/ZM/Store/FileSystem.php
@@ -82,7 +82,7 @@ class FileSystem
*/
public static function createDir(string $path): void
{
- if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) {
+ if (!is_dir($path) && !mkdir($path, 0755, true) && !is_dir($path)) {
throw new \RuntimeException(sprintf('无法建立目录:%s', $path));
}
}
diff --git a/src/ZM/Utils/CodeGenerator/PluginGenerator.php b/src/ZM/Utils/CodeGenerator/PluginGenerator.php
new file mode 100644
index 00000000..3396afb3
--- /dev/null
+++ b/src/ZM/Utils/CodeGenerator/PluginGenerator.php
@@ -0,0 +1,84 @@
+plugin_dir);
+
+ // 创建插件目录
+ $plugin_base_dir = $this->plugin_dir . '/' . $this->name;
+ FileSystem::createDir($plugin_base_dir);
+
+ // 这里开始写入 zmplugin.json
+ // 创建插件信息文件
+ $zmplugin['name'] = $this->name;
+ // 设置版本
+ if ($options['plugin-version'] !== null) {
+ $zmplugin['version'] = $options['plugin-version'];
+ }
+ // 设置作者
+ if ($options['author'] !== null) {
+ $zmplugin['author'] = $options['author'];
+ }
+ // 判断单文件还是 psr-4 类型
+ if ($options['type'] === 'file') {
+ // 设置入口文件为 main.php
+ $zmplugin['main'] = 'main.php';
+ }
+ // 到这里就可以写入文件了
+ file_put_contents(zm_dir($plugin_base_dir . '/zmplugin.json'), json_encode($zmplugin, JSON_PRETTY_PRINT));
+
+ // 接着写入 main.php
+ if ($options['type'] === 'file') {
+ $template = file_get_contents(zm_dir(FRAMEWORK_ROOT_DIR . '/src/Templates/main.php.template'));
+ $replace = ['{name}' => $this->name];
+ $main_php = str_replace(array_keys($replace), array_values($replace), $template);
+ file_put_contents(zm_dir($plugin_base_dir . '/main.php'), $main_php);
+ } else {
+ // 如果是 psr4 就复杂一点,但也不麻烦
+ // 先创建 src 目录
+ FileSystem::createDir($plugin_base_dir . '/src');
+ // 再创建 src/PluginMain.php
+ $template = file_get_contents(zm_dir(FRAMEWORK_ROOT_DIR . '/src/Templates/PluginMain.php.template'));
+ $replace = [
+ '{name}' => $this->name,
+ '{namespace}' => $options['namespace'],
+ '{class}' => $this->convertClassName(),
+ ];
+ $main_php = str_replace(array_keys($replace), array_values($replace), $template);
+ file_put_contents(zm_dir($plugin_base_dir . '/src/PluginMain.php'), $main_php);
+ // 写入 composer.json
+ $composer_json = [
+ 'autoload' => [
+ 'psr-4' => [
+ $options['namespace'] . '\\' => 'src/',
+ ],
+ ],
+ ];
+ file_put_contents(zm_dir($plugin_base_dir . '/composer.json'), json_encode($composer_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
+ // TODO: 寻找 PHP 运行环境和 Composer 是否在当前目录的情况
+ chdir($plugin_base_dir);
+ passthru('composer dump-autoload');
+ chdir(WORKING_DIR);
+ }
+ }
+
+ public function convertClassName(): string
+ {
+ $name = $this->name;
+ $string = str_replace(['-', '_'], ' ', $name);
+ return str_replace(' ', '', ucwords($string));
+ }
+}