diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 0225d218..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,3 +0,0 @@
-FROM zmbot/swoole:latest
-
-# TODO: auto-setup entrypoint
diff --git a/SECURITY.md b/SECURITY.md
deleted file mode 100644
index ceccc54f..00000000
--- a/SECURITY.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Security Policy
-
-## Supported Versions
-
-| Version | Supported |
-| ------- | ------------------ |
-| 2.0 | :white_check_mark: |
-| 1.6.x | :white_check_mark: |
-| 1.1.x | :x: |
-| 1.0.x | :x: |
-
-## Reporting a Vulnerability
-
-If you find a bug which is safety related, you should post a new issue named **Security Issue**, and I will check it as soon as possible.
diff --git a/config/global.php b/config/global.php
index 5a55d125..208f2770 100644
--- a/config/global.php
+++ b/config/global.php
@@ -39,7 +39,7 @@ $config['light_cache'] = [
'size' => 512, //最多允许储存的条数(需要2的倍数)
'max_strlen' => 32768, //单行字符串最大长度(需要2的倍数)
'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多)
- 'persistence_path' => $config['zm_data'].'_cache.json',
+ 'persistence_path' => $config['zm_data'] . '_cache.json',
'auto_save_interval' => 900
];
@@ -112,22 +112,19 @@ $config['server_event_handler_class'] = [
// 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类
];
-/** 服务器启用的外部第三方和内部插件 */
-$config['modules'] = [
- 'onebot' => [ // 机器人解析模块,关闭后无法使用如@CQCommand等注解
- 'status' => true,
- 'single_bot_mode' => false
- ],
- 'http_proxy_server' => [ // 一个内置的简单HTTP代理服务器,目前还没有认证功能,预计2.4.0版本完成
- 'status' => false,
- 'host' => '0.0.0.0',
- 'port' => 8083,
- 'swoole_set_override' => [
- 'backlog' => 128,
- 'buffer_output_size' => 1024 * 1024 * 128,
- 'socket_buffer_size' => 1024 * 1024 * 1
- ]
- ],
+/** 机器人解析模块,关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
+$config['onebot'] = [
+ 'status' => true,
+ 'single_bot_mode' => false,
+ 'message_level' => 99999
+];
+
+/** 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) */
+$config['remote_terminal'] = [
+ 'status' => false,
+ 'host' => '127.0.0.1',
+ 'port' => 20002,
+ 'token' => ''
];
return $config;
diff --git a/docs/FAQ.md b/docs/FAQ.md
index c974f108..a67fdd0e 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -1,3 +1,23 @@
# FAQ
-这里会写一些常见的疑难解答。
\ No newline at end of file
+这里会写一些常见的疑难解答。
+
+## 启动时报错 Address already in use
+
+1. 检查是否开启了两次框架,每个端口只能开启一个框架。
+2. 如果是之前已经在 20001 端口或者你设置了别的应用同样占用此端口,更换配置文件 `global.php` 中的 port 即可。
+3. 如果是之前框架成功启动,但是使用 Ctrl+C 停止后再次启动导致的报错,请根据下面的步骤来检查是否存在僵尸进程。
+
+- 如果系统内装有 `htop`,可以直接在 `htop` 中开启 Tree 模式并使用 filter 过滤 php,检查残留的框架进程。
+- 如果系统没有 `htop`,使用 `ps aux | grep vendor/bin/start | grep -v grep` 如果存在进程,请使用以下命令尝试杀掉:
+
+```bash
+# 如果确定框架的数据都已保存且没有需要保存的缓存数据,直接杀掉 SIGKILL 即可,输入下面这条
+ps aux | grep vendor/bin/start | grep -v grep | awk '{print $2}' | xargs kill -9
+
+# 如果不确定框架是不是还继续运行,想尝试正常关闭(走一遍储存保存数据的事件),使用下面这条
+# 首先使用 'ps aux | grep vendor/bin/start | grep -v grep' 找到进程中第二列最小的pid
+# 然后使用下面的这条命令,假设最小的pid是23643
+kill -INT 23643
+# 如果使用 ps aux 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死
+```
\ No newline at end of file
diff --git a/docs/advanced/inside-class.md b/docs/advanced/inside-class.md
index b71a30b9..fb300250 100644
--- a/docs/advanced/inside-class.md
+++ b/docs/advanced/inside-class.md
@@ -27,3 +27,44 @@
TODO:先放一放。
```
+## ZM\Entity\MatchObject
+
+此类是调用方法 `MessageUtil::matchCommand()` 返回的对象体,含有匹配成功与否和匹配到的注解相关的信息。
+
+### 属性
+
+- `$match`:`bool` 类型,返回匹配是否成功
+- `$object`:`CQCommand` 注解类,如果匹配成功则返回对应的 `@CQCommand` 信息
+- `match`:`array` 类型,如果匹配成功则返回匹配到的参数
+
+```php
+// 假设我有一个注解事件 @CQCommand(match="你好"),绑定的函数是 \Module\Example\Hello 下的 hello123()
+
+$obj = MessageUtil::matchCommand("你好 我叫顺溜 我今年二十八", ctx()->getData());
+/* 以下是返回信息,仅供参考
+$obj->match ==> true
+$obj->object ==> \ZM\Annotation\CQ\CQCommand: (
+ match: "你好",
+ pattern: "",
+ regex: "",
+ start_with: "",
+ end_with: "",
+ keyword: "",
+ alias: [],
+ message_type: "",
+ user_id: 0,
+ group_id: 0,
+ discuss_id: 0,
+ level: 20,
+ method: "hello123",
+ class: \Module\Example\Hello::class
+)
+$obj->match ==> [
+ "我叫顺溜",
+ "我今年二十八"
+]
+*/
+```
+
+
+
diff --git a/docs/advanced/task-worker.md b/docs/advanced/task-worker.md
new file mode 100644
index 00000000..18495898
--- /dev/null
+++ b/docs/advanced/task-worker.md
@@ -0,0 +1,3 @@
+# 使用 TaskWorker 进程处理密集运算
+
+> 新开个坑,有时间补上。(__填坑标记__)
\ No newline at end of file
diff --git a/docs/assets/img/image-20210321193956832.png b/docs/assets/img/image-20210321193956832.png
new file mode 100644
index 00000000..e69de29b
diff --git a/docs/component/global-functions.md b/docs/component/global-functions.md
index f4e6e37e..484dd7f5 100644
--- a/docs/component/global-functions.md
+++ b/docs/component/global-functions.md
@@ -134,10 +134,6 @@ set_coroutine_params(["data" => [
别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。
-## zm_debug()
-
-同 `Console::debug($msg)`。
-
## zm_sleep()
协程版 `sleep()` 函数。
@@ -255,4 +251,62 @@ bot()->sendPrivateMsg(123456, "你好啊!!");
定义:`getAllFdByConnectType(string $type = 'default'): array`
-当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
\ No newline at end of file
+当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
+
+## zm_dump()
+
+更漂亮地输出变量值,可替代 `var_dump()`。
+
+```php
+class Pass {
+ public $foo = 123;
+ public $bar = ["a", "b"];
+}
+$pass = new Pass();
+$pass->obj = true;
+zm_dump($pass);
+```
+
+
+
+## zm_config()
+
+> v2.4.0 起可用。
+
+同 `ZMConfig::get()`。
+
+定义:`zm_config($name, $key = null)`。
+
+有关 ZMConfig 模块的说明,见 [指南 - 基本配置](/guide/basic-config/)。
+
+```php
+zm_config("global"); //等同于 ZMConfig::get("global");
+zm_config("global", "swoole"); //等同于 ZMConfig::get("global", "swoole");
+```
+
+## zm_info()
+
+> v2.4.0 起可用。(下面的 log 类也一样)
+
+同 `Console::info($msg)`。
+
+## zm_debug()
+
+同 `Console::debug($msg)`。
+
+## zm_warning()
+
+同 `Console::warning($msg)`。
+
+## zm_success()
+
+同 `Console::success($msg)`。
+
+## zm_error()
+
+同 `Console::error($msg)`。
+
+## zm_verbose()
+
+同 `Console::verbose($msg)`。
+
diff --git a/docs/component/message-util.md b/docs/component/message-util.md
new file mode 100644
index 00000000..72f6507a
--- /dev/null
+++ b/docs/component/message-util.md
@@ -0,0 +1,89 @@
+# MessageUtil 消息处理工具类
+
+类定义:`\ZM\Utils\MessageUtil`
+
+> 2.3.0 版本起可用。
+
+这里放置一些机器人聊天消息处理的便捷静态方法,例如下载图片等。
+
+## 方法
+
+### downloadCQImage()
+
+下载用户消息中所带的所有图片,并返回文件路径。
+
+定义:`downloadCQImage($msg, $path = null)`
+
+参数 `$msg` 为带图片的用户消息,例如 `你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]`
+
+参数 `$path` 为图片下载的路径,如果不填(默认 null)则指定为 `zm_data/images/` 目录,且不存在会自动创建。
+
+```php
+$r = MessageUtil::downloadCQImage("你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]");
+/*
+$r == [
+ "/path-to/zhamao-framework/zm_data/images/a.jpg"
+];
+*/
+```
+
+如果返回的是空数组 `[ ]`,则表明消息中没有图片。如果返回的是 `false`,则表明其中至少一张下载失败或路径有误。
+
+### containsImage()
+
+检查消息中是否含有图片。
+
+定义:`containsImage($msg)`
+
+返回:`bool`,你懂的,true 就是有,false 就没有。
+
+```php
+MessageUtil::containsImage("[CQ:image,file=a.jpg,url=http://xxx]"); // true
+MessageUtil::containsImage("[CQ:face,id=140] 咦,这是一条带表情的消息"); // false
+```
+
+### getImageCQFromLocal()
+
+通过文件路径获取图片的发送 CQ 码。
+
+定义:`getImageCQFromLocal($file, $type = 0)`
+
+参数 `$file` 为图片的绝对路径。
+
+返回:图片的 CQ 码,如 `[CQ:image,file=xxxxx]`
+
+参数 `$type`:
+
+- `0`:以 base64 的方式发送图片,返回结果如 `[CQ:image,file=base64://xxxxxx]`
+- `1`:以 `file://` 本地文件的方式发送图片,返回结果如 `[CQ:image,file=file:///path-to/images/a.jpg]`
+- `2`:返回图片的 http:// CQ 码(默认为 /images/ 路径就是文件对应所在的目录),如 `[CQ:image,file=http://127.0.0.1:20001/images/a.jpg]`
+
+### splitCommand()
+
+切割用户消息为数组形式(`@CQCommand` 就是使用此方式切割的)
+
+定义:`splitCommand($msg): array`
+
+返回:数组,切分后的。
+
+!!! tip "为什么不直接使用 explode 呢"
+
+ 因为 `explode()` 只会简单粗暴的切割字符串,假设用户输入的消息中两个词中间有多个空格,则会有空的词出现。例如 `你好 我是一个长空格`。此函数会将多个空格当作一个空格来对待。
+
+```php
+MessageUtil::splitCommand("你好 我是傻瓜\n我是傻瓜二号"); // ["你好","我是傻瓜","我是傻瓜二号"]
+MessageUtil::splitCommand("我有 三个空格"); // ["我有","三个空格"]
+```
+
+### matchCommand()
+
+匹配一条消息到 `@CQCommand` 规则的注解事件,返回要执行的类和函数位置。
+
+定义:`matchCommand($msg, $obj)`
+
+参数 `$msg` 为消息内容。
+
+参数 `$obj` 为事件的对象,可使用 `ctx()->getData()` 获取原先的事件体(仅限 OneBot 消息类型事件中使用)
+
+返回:`\ZM\Entity\MatchObject` 对象,含有匹配成功与否,匹配到的注解对象,匹配到的分割词等,见 []
+
diff --git a/docs/component/route-manager.md b/docs/component/route-manager.md
new file mode 100644
index 00000000..698e125d
--- /dev/null
+++ b/docs/component/route-manager.md
@@ -0,0 +1,54 @@
+# HTTP 路由管理
+
+HTTP 路由管理器用作管理炸毛框架内 `@RequestMapping` 和静态目录的路由操作的,可在运行过程中编写添加路由。
+
+类定义:`\ZM\Http\RouteManager`
+
+> 2.3.0 版本起可用。
+
+!!! warning "注意"
+
+ 因为炸毛框架的路由实现是不基于跨进程的共享内存的,所以每次使用这里面的工具函数都需要单独在所有 Worker 进程中执行一次,最好的办法就是在启动框架时执行(`@OnStart(-1)` 即可,代表此注解事件将在每个工作进程中都被执行一次)。
+
+## 方法
+
+### importRouteByAnnotation()
+
+通过注解类导入路由。(注:此方法一般为框架内部使用)
+
+定义:`importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations)`
+
+参数 `$vss`:RequestMapping 注解类,类中定义 `route` 和 `request_method` 即可。
+
+参数 `$method, $class`:执行的目标注解事件函数位置,比如 `$class = \Module\Example\Hello::class`,`$method = 'hitokoto'`。
+
+参数 `$methods_annotations`:需要绑定的 Controller 注解类数组,一般数组内建议只带有一个 Controller,如 `[$controller]`。
+
+### addStaticFileRoute()
+
+添加一个单目录(此目录下无子目录,只有文件)并绑定为一个路由。
+
+定义:`addStaticFileRoute($route, $path)`
+
+参数 `$route`:绑定的目标路由,如 `/images/`。
+
+参数 `$path`:绑定的文件目录位置,如 `/root/zhamao-framework-starter/zm_data/images/`。
+
+```php
+/**
+ * @OnStart(-1)
+ */
+public function onStart() {
+ RouteManager::addStaticFileRoute("/images/", DataProvider::getDataFolder()."images/");
+}
+```
+
+## 属性
+
+### RouteManager::$routes
+
+此为存放路由树的变量,请谨慎操作。
+
+定义:`\Symfony\Component\Routing\RouteCollection | null`
+
+炸毛框架使用了 Symfony 框架的 route 组件,有关详情请查阅 [文档](https://symfony.com/doc/current/routing.html)。
\ No newline at end of file
diff --git a/docs/component/task-worker.md b/docs/component/task-worker.md
new file mode 100644
index 00000000..77c955b8
--- /dev/null
+++ b/docs/component/task-worker.md
@@ -0,0 +1,26 @@
+# TaskManager 工作进程管理
+
+此类管理的是 TaskWorker 相关工作。有关使用 TaskWorker 的教程,见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)
+
+类定义:`\ZM\Utils\TaskManager`
+
+使用 TaskWorker 需要先在 `global.php` 配置文件中开启!
+
+## 方法
+
+### runTask()
+
+在 TaskWorker 运行任务。
+
+定义:`runTask($task_name, $timeout = -1, ...$params)`
+
+参数 `$task_name`:对应 `@OnTask` 注解绑定的任务函数。
+
+参数 `$timeout`:等待任务函数最长运行的时间(秒),如果超过此时间将返回 false。
+
+参数 `剩余`:将变量传入 TaskWorker 进程,除 Closure,资源类型外,可序列化的变量均可。
+
+```php
+TaskManager::runTask("heavy_task", 100, "param1", "param2");
+```
+
diff --git a/docs/component/zmutil.md b/docs/component/zmutil.md
index 1ee6cb78..68f0d795 100644
--- a/docs/component/zmutil.md
+++ b/docs/component/zmutil.md
@@ -21,3 +21,47 @@ class ASD{
ZMUtil::getModInstance(ASD::class)->test = 5;
```
+## ZMUtil::getReloadableFiles()
+
+返回可通过热重启(reload)来重新加载的 php 文件列表。
+
+以下是示例模块下的例子(直接拉取最新的框架源码并运行框架后获取的)。
+
+```php
+array:31 [
+ 94 => "src/ZM/Context/Context.php"
+ 95 => "src/ZM/Context/ContextInterface.php"
+ 96 => "src/ZM/Annotation/AnnotationParser.php"
+ 97 => "src/Custom/Annotation/Example.php"
+ 98 => "src/ZM/Annotation/Interfaces/CustomAnnotation.php"
+ 99 => "src/Module/Example/Hello.php"
+ 100 => "src/ZM/Annotation/Swoole/OnStart.php"
+ 101 => "src/ZM/Annotation/CQ/CQCommand.php"
+ 102 => "src/ZM/Annotation/Interfaces/Level.php"
+ 103 => "src/ZM/Annotation/Command/TerminalCommand.php"
+ 104 => "src/ZM/Annotation/Http/RequestMapping.php"
+ 105 => "src/ZM/Annotation/Http/RequestMethod.php"
+ 106 => "src/ZM/Annotation/Http/Middleware.php"
+ 107 => "src/ZM/Annotation/Interfaces/ErgodicAnnotation.php"
+ 108 => "src/ZM/Annotation/Swoole/OnOpenEvent.php"
+ 109 => "src/ZM/Annotation/Swoole/OnSwooleEventBase.php"
+ 110 => "src/ZM/Annotation/Interfaces/Rule.php"
+ 111 => "src/ZM/Annotation/Swoole/OnCloseEvent.php"
+ 112 => "src/ZM/Annotation/Swoole/OnRequestEvent.php"
+ 113 => "src/ZM/Http/RouteManager.php"
+ 114 => "vendor/symfony/routing/RouteCollection.php"
+ 115 => "vendor/symfony/routing/Route.php"
+ 116 => "src/Module/Middleware/TimerMiddleware.php"
+ 117 => "src/ZM/Http/MiddlewareInterface.php"
+ 118 => "src/ZM/Annotation/Http/MiddlewareClass.php"
+ 119 => "src/ZM/Annotation/Http/HandleBefore.php"
+ 120 => "src/ZM/Annotation/Http/HandleAfter.php"
+ 121 => "src/ZM/Annotation/Http/HandleException.php"
+ 122 => "src/ZM/Event/EventManager.php"
+ 123 => "src/ZM/Annotation/Swoole/OnSwooleEvent.php"
+ 124 => "src/ZM/Event/EventDispatcher.php"
+]
+```
+
+> 为什么不能重载所有文件?因为框架是多进程模型,而重载相当于只重新启动了一次 Worker 进程,Manager 和 Master 进程未重启,所以被 Manager、Master 进程已经加载的 PHP 文件无法使用 reload 命令重新加载。详见 [进阶 - 进程间隔离](/advanced/multi-process/#_5)。
+
diff --git a/docs/event/framework-annotations.md b/docs/event/framework-annotations.md
index 874d227d..db8e3bcc 100644
--- a/docs/event/framework-annotations.md
+++ b/docs/event/framework-annotations.md
@@ -262,6 +262,27 @@
无。
+## TerminalCommand()
+
+添加一个远程终端的自定义命令。(2.4.0 版本起可用)
+
+### 属性
+
+| 类型 | 值 |
+| ---------- | --------------------------------------- |
+| 名称 | `@TerminalCommand` |
+| 触发前提 | 连接到远程终端可触发 |
+| 命名空间 | `ZM\Annotation\Command\TerminalCommand` |
+| 适用位置 | 方法 |
+| 返回值处理 | 无 |
+
+### 注解参数
+
+| 参数名称 | 参数范围 | 默认 |
+| ----------- | ------------------------------ | ---- |
+| command | `string`,**必填**,命令字符串 | |
+| description | `string`,要显示的帮助文本 | 空 |
+
## 示例1(机器人连接框架后输出信息)
```php
@@ -406,3 +427,6 @@ public function onCrawl() {
}
```
+## 示例6(创建一个远程终端命令并调试框架)
+
+> 开个坑,以后填。(__填坑标记__)
\ No newline at end of file
diff --git a/docs/update/config.md b/docs/update/config.md
new file mode 100644
index 00000000..5698c724
--- /dev/null
+++ b/docs/update/config.md
@@ -0,0 +1,25 @@
+# 配置文件变更记录
+
+这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。
+
+## v2.4.0 (build 400)
+- 调整 `$config['modules']['onebot']` 配置项到 `$config['onebot']`,旧版本的此段会向下兼容,建议更新,
+- 新增 `$config['remote_terminal']` 远程终端的配置项,新增此段即可。
+
+更新部分:
+```php
+/** 机器人解析模块,关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
+$config['onebot'] = [
+ 'status' => true,
+ 'single_bot_mode' => false,
+ 'message_level' => 99999
+];
+
+/** 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) */
+$config['remote_terminal'] = [
+ 'status' => false,
+ 'host' => '127.0.0.1',
+ 'port' => 20002,
+ 'token' => ''
+];
+```
\ No newline at end of file
diff --git a/docs/update/v2.md b/docs/update/v2.md
index 057086e5..bb9855b4 100644
--- a/docs/update/v2.md
+++ b/docs/update/v2.md
@@ -1,5 +1,39 @@
# 更新日志(v2 版本)
+## v2.4.0(build 400)
+
+> 更新时间:2021.3.24
+
+- 新增:检查全局配置文件的命令
+- 新增:全局配置文件更新记录
+- 依赖变更:**Swoole 最低版本需要 4.5.0**
+- 优化:reload 和 stop 命令重载和停止框架的逻辑
+- 新增:`$_running_annotation` 变量,可在注解事件中的类使用
+- 新增:远程终端(Remote Terminal),弥补原来删掉的本地终端,通过 nc 命令连接即可
+- 新增:启动参数 `--worker-num`,`--task-worker-num`,`--remote-terminal`
+- 更新:全局配置文件结构
+- 新增:Swoole 计时器报错处理
+- 新增:全局方法(`zm_dump()`,`zm_error()`,`zm_warning()`,`zm_info()`,`zm_success()`,`zm_verbose()`,`zm_debug()`,`zm_config()`)
+- 新增:示例模块的图灵机器人和 at 机器人的处理函数
+- 新增:MessageUtil 工具类新增 `isAtMe(), splitCommand(), matchCommand()` 方法
+- 新增:ProcessManager 进程管理类新增 `workerAction(), sendActionToWorker(), resumeAllWorkerCoroutines()` 方法
+- 优化:CQCommand 的匹配逻辑
+- 新增:支持添加自定义远程终端指令的 `@TerminalCommand` 注解
+- 新增:图灵机器人 API 封装函数
+- 新增:ZMUtil 工具杂项类 `getReloadableFiles()` 函数
+
+
+
+
+
+
+- 修复:部分内部存储存在的内存泄漏问题
+- 新增:remote_terminal(远程终端)内置模块,可以用 nc 直接远程连接框架(弥补原先删除掉的终端输入)
+- 新增:`DataProvider::getReloadableFiles()` 返回可通过热重启(reload)来重新加载的 php 文件列表(多用于调试)
+- 兼容性变更:**要求最低 Swoole 版本为 4.5.0**
+- 优化:reload 和 stop 重载和停止框架的逻辑,防止卡死
+- 新增:远程终端命令支持自定义添加(`@TerminalCommand` 注解)
+
## v2.3.5 (build 398)
> 更新时间:2021.3.23
@@ -15,7 +49,6 @@
- 规范代码,修复一个小报错的 bug
-
## v2.3.0
> 更新时间:2021.3.16
diff --git a/mkdocs.yml b/mkdocs.yml
index 9c7613ef..28aee039 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -72,9 +72,12 @@ nav:
- 事件分发器: event/event-dispatcher.md
- 框架组件:
- 框架组件: component/index.md
- - 机器人 API: component/robot-api.md
- - CQ 码(多媒体消息): component/cqcode.md
- 上下文: component/context.md
+ - 聊天机器人组件:
+ - 机器人 API: component/robot-api.md
+ - CQ 码(多媒体消息): component/cqcode.md
+ - 机器人消息处理: component/message-util.md
+ - Token 验证: component/access-token.md
- 存储:
- LightCache 轻量缓存: component/light-cache.md
- MySQL 数据库: component/mysql.md
@@ -82,13 +85,15 @@ nav:
- ZMAtomic 原子计数器: component/atomics.md
- SpinLock 自旋锁: component/spin-lock.md
- 文件管理: component/data-provider.md
+ - HTTP 服务器工具类:
+ - HTTP 和 WebSocket 客户端: component/zmrequest.md
+ - HTTP 路由管理: component/route-manager.md
- 协程池: component/coroutine-pool.md
- 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md
- - HTTP 和 WebSocket 客户端: component/zmrequest.md
- Console 终端: component/console.md
- - Token 验证: component/access-token.md
+ - TaskWorker 管理: component/task-worker.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md
@@ -97,6 +102,7 @@ nav:
- 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md
+ - TaskWorker 提高并发: advanced/task-worker.md
- 开发实战教程:
- 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md
diff --git a/resources/images/logo.png b/resources/images/logo.png
deleted file mode 100644
index c7e36850..00000000
Binary files a/resources/images/logo.png and /dev/null differ
diff --git a/resources/images/logo2.png b/resources/images/logo2.png
deleted file mode 100644
index bd828823..00000000
Binary files a/resources/images/logo2.png and /dev/null differ
diff --git a/src/Module/Example/Hello.php b/src/Module/Example/Hello.php
index 5abf3a96..78be3122 100644
--- a/src/Module/Example/Hello.php
+++ b/src/Module/Example/Hello.php
@@ -2,17 +2,23 @@
namespace Module\Example;
+use ZM\Annotation\CQ\CQBefore;
+use ZM\Annotation\CQ\CQMessage;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
+use ZM\API\CQ;
+use ZM\API\TuringAPI;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\RequestMapping;
use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException;
+use ZM\Module\QQBot;
use ZM\Requests\ZMRequest;
+use ZM\Utils\MessageUtil;
use ZM\Utils\ZMUtil;
/**
@@ -67,6 +73,42 @@ class Hello
return $obj["hitokoto"] . "\n----「" . $obj["from"] . "」";
}
+ /**
+ * 图灵机器人的内置实现,在tuling123.com申请一个apikey填入下方变量即可。
+ * @CQCommand(start_with="机器人",end_with="机器人",message_type="group")
+ * @CQMessage(message_type="private",level=1)
+ */
+ public function turingAPI() {
+ $user_id = ctx()->getUserId();
+ $api = ""; // 请在这里填入你的图灵机器人的apikey
+ if ($api === "") return false; //如果没有填入apikey则此功能关闭
+ if (($this->_running_annotation ?? null) instanceof CQCommand) {
+ $msg = ctx()->getFullArg("我在!有什么事吗?");
+ } else {
+ $msg = ctx()->getMessage();
+ }
+ ctx()->setMessage($msg);
+ if (MessageUtil::matchCommand($msg, ctx()->getData())->status === false) {
+ return TuringAPI::getTuringMsg($msg, $user_id, $api);
+ } else {
+ QQBot::getInstance()->handle(ctx()->getData(), ctx()->getCache("level") + 1);
+ return true;
+ }
+ }
+
+ /**
+ * 响应at机器人的消息
+ * @CQBefore("message")
+ */
+ public function changeAt() {
+ if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) {
+ $msg = str_replace(CQ::at(ctx()->getRobotId()), "", ctx()->getMessage());
+ ctx()->setMessage("机器人" . $msg);
+ Console::info(ctx()->getMessage());
+ }
+ return true;
+ }
+
/**
* 一个简单随机数的功能demo
* 问法1:随机数 1 20
diff --git a/src/ZM/API/TuringAPI.php b/src/ZM/API/TuringAPI.php
new file mode 100644
index 00000000..ab5de163
--- /dev/null
+++ b/src/ZM/API/TuringAPI.php
@@ -0,0 +1,118 @@
+ 0,
+ "userInfo" => [
+ "apiKey" => $api,
+ "userId" => $user_id
+ ]
+ ];
+ if ($msg != "") {
+ $content["perception"]["inputText"]["text"] = $msg;
+ }
+ $msg = trim($msg);
+ if (mb_strlen($msg) < 1 && !isset($url)) return "请说出你想说的话";
+ if (isset($url)) {
+ $content["perception"]["inputImage"]["url"] = $url;
+ $content["reqType"] = 1;
+ }
+ if (!isset($content["perception"])) return "请说出你想说的话";
+ $client = new Client("openapi.tuling123.com", 80);
+ $client->setHeaders(["Content-type" => "application/json"]);
+ $client->post("/openapi/api/v2", json_encode($content, JSON_UNESCAPED_UNICODE));
+ $api_return = json_decode($client->body, true);
+ if (!isset($api_return["intent"]["code"])) return "XD 哎呀,我脑子突然短路了,请稍后再问我吧!";
+ $status = self::getResultStatus($api_return);
+ if ($status !== true) {
+ if ($status == "err:输入文本内容超长(上限150)") return "你的话太多了!!!";
+ if ($api_return["intent"]["code"] == 4003) {
+ return "哎呀,我刚才有点走神了,可能忘记你说什么了,可以重说一遍吗";
+ }
+ Console::error("图灵机器人发送错误!\n错误原始内容:" . $origin . "\n来自:" . $user_id . "\n错误信息:" . $status);
+ //echo json_encode($r, 128|256);
+ return "哎呀,我刚才有点走神了,要不一会儿换一种问题试试?";
+ }
+ $result = $api_return["results"];
+ //Console::info(Console::setColor(json_encode($result, 128 | 256), "green"));
+ $final = "";
+ foreach ($result as $k => $v) {
+ switch ($v["resultType"]) {
+ case "url":
+ $final .= "\n" . $v["values"]["url"];
+ break;
+ case "text":
+ $final .= "\n" . $v["values"]["text"];
+ break;
+ case "image":
+ $final .= "\n" . CQ::image($v["values"]["image"]);
+ break;
+ }
+ }
+ return trim($final);
+ }
+
+ public static function getResultStatus($r) {
+ switch ($r["intent"]["code"]) {
+ case 5000:
+ return "err:无解析结果";
+ case 4000:
+ case 6000:
+ return "err:暂不支持该功能";
+ case 4001:
+ return "err:加密方式错误";
+ case 4005:
+ case 4002:
+ return "err:无功能权限";
+ case 4003:
+ return "err:该apikey没有可用请求次数";
+ case 4007:
+ return "err:apikey不合法";
+ case 4100:
+ return "err:userid获取失败";
+ case 4200:
+ return "err:上传格式错误";
+ case 4300:
+ return "err:批量操作超过限制";
+ case 4400:
+ return "err:没有上传合法userid";
+ case 4500:
+ return "err:userid申请个数超过限制";
+ case 4600:
+ return "err:输入内容为空";
+ case 4602:
+ return "err:输入文本内容超长(上限150)";
+ case 7002:
+ return "err:上传信息失败";
+ case 8008:
+ return "err:服务器错误";
+ default:
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ZM/Annotation/Command/TerminalCommand.php b/src/ZM/Annotation/Command/TerminalCommand.php
new file mode 100644
index 00000000..30034cb8
--- /dev/null
+++ b/src/ZM/Annotation/Command/TerminalCommand.php
@@ -0,0 +1,27 @@
+setDescription("检查配置文件是否和框架当前版本有更新");
+ }
+
+ /** @noinspection PhpIncludeInspection */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if (LOAD_MODE !== 1) {
+ $output->writeln("仅限在Composer依赖模式中使用此命令!");
+ return Command::FAILURE;
+ }
+ $current_cfg = WORKING_DIR . "/config/";
+ $remote_cfg = include_once $current_cfg . "/vendor/zhamao/framework/config/global.php";
+ if (file_exists($current_cfg . "global.php")) {
+ $this->check($remote_cfg, "global.php", $output);
+ }
+ if (file_exists($current_cfg . "global.development.php")) {
+ $this->check($remote_cfg, "global.development.php", $output);
+ }
+ if (file_exists($current_cfg . "global.staging.php")) {
+ $this->check($remote_cfg, "global.staging.php", $output);
+ }
+ if (file_exists($current_cfg . "global.production.php")) {
+ $this->check($remote_cfg, "global.production.php", $output);
+ }
+ if ($this->need_update === true) {
+ $output->writeln("有配置文件需要更新,详情见文档 https://framework.zhamao.xin/update/config.md");
+ }
+ return Command::SUCCESS;
+ }
+
+ private function check($remote, $local, OutputInterface $out) {
+ $local_file = include_once WORKING_DIR . "/config/".$local;
+ foreach($remote as $k => $v) {
+ if (!isset($local_file[$k])) {
+ $out->writeln("配置文件 ".$local . " 需要更新!");
+ $this->need_update = true;
+ return;
+ }
+ }
+ }
+}
diff --git a/src/ZM/Command/DaemonStopCommand.php b/src/ZM/Command/DaemonStopCommand.php
index 3a10f45b..98245cb1 100644
--- a/src/ZM/Command/DaemonStopCommand.php
+++ b/src/ZM/Command/DaemonStopCommand.php
@@ -18,7 +18,7 @@ class DaemonStopCommand extends DaemonCommand
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
- system("kill -TERM " . intval($this->daemon_file["pid"]));
+ system("kill -INT " . intval($this->daemon_file["pid"]));
unlink(DataProvider::getWorkingDir() . "/.daemon_pid");
$output->writeln("成功停止!");
return Command::SUCCESS;
diff --git a/src/ZM/Command/RunServerCommand.php b/src/ZM/Command/RunServerCommand.php
index c5a93d6d..9b54b721 100644
--- a/src/ZM/Command/RunServerCommand.php
+++ b/src/ZM/Command/RunServerCommand.php
@@ -23,8 +23,11 @@ class RunServerCommand extends Command
new InputOption("log-error", null, null, "调整消息等级到error (log-level=0)"),
new InputOption("log-theme", null, InputOption::VALUE_REQUIRED, "改变终端的主题配色"),
new InputOption("disable-console-input", null, null, "禁止终端输入内容 (废弃)"),
+ new InputOption("remote-terminal", null, null, "启用远程终端,配置使用global.php中的"),
new InputOption("disable-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"),
+ new InputOption("worker-num", null, InputOption::VALUE_REQUIRED, "启动框架时运行的 Worker 进程数量"),
+ new InputOption("task-worker-num", null, InputOption::VALUE_REQUIRED, "启动框架时运行的 TaskWorker 进程数量"),
new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"),
new InputOption("show-php-ver", null, null, "启动时显示PHP和Swoole版本"),
new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"),
diff --git a/src/ZM/ConsoleApplication.php b/src/ZM/ConsoleApplication.php
index 74fc50b9..21802594 100644
--- a/src/ZM/ConsoleApplication.php
+++ b/src/ZM/ConsoleApplication.php
@@ -5,6 +5,7 @@ namespace ZM;
use Exception;
+use ZM\Command\CheckConfigCommand;
use ZM\Command\DaemonReloadCommand;
use ZM\Command\DaemonStatusCommand;
use ZM\Command\DaemonStopCommand;
@@ -18,8 +19,8 @@ use ZM\Utils\DataProvider;
class ConsoleApplication extends Application
{
- const VERSION_ID = 398;
- const VERSION = "2.3.5";
+ const VERSION_ID = 399;
+ const VERSION = "2.4.0";
public function __construct(string $name = 'UNKNOWN') {
define("ZM_VERSION_ID", self::VERSION_ID);
@@ -75,6 +76,9 @@ class ConsoleApplication extends Application
new InitCommand(), //初始化用的,用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令
]);
+ if (LOAD_MODE === 1) {
+ $this->add(new CheckConfigCommand());
+ }
/*
$command_register = ZMConfig::get("global", "command_register_class") ?? [];
foreach ($command_register as $v) {
@@ -100,7 +104,7 @@ class ConsoleApplication extends Application
private function selfCheck(): bool {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19\n");
- if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
+ if (version_compare(SWOOLE_VERSION, "4.5.0") == -1) die("You must install swoole version >= 4.5.0 !");
if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required.");
return true;
}
diff --git a/src/ZM/Context/Context.php b/src/ZM/Context/Context.php
index c40b3443..1fe5bf7a 100644
--- a/src/ZM/Context/Context.php
+++ b/src/ZM/Context/Context.php
@@ -216,7 +216,7 @@ class Context implements ContextInterface
* @throws WaitTimeoutException
*/
public function getArgs($mode, $prompt_msg) {
- $arg = ctx()->getCache("match");
+ $arg = ctx()->getCache("match") ?? [];
switch ($mode) {
case ZM_MATCH_ALL:
$p = $arg;
diff --git a/src/ZM/Entity/MatchResult.php b/src/ZM/Entity/MatchResult.php
new file mode 100644
index 00000000..d345f797
--- /dev/null
+++ b/src/ZM/Entity/MatchResult.php
@@ -0,0 +1,17 @@
+_running_annotation = $v;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$this->store = $q_o->$q_f(...$params);
} catch (Exception $e) {
@@ -188,6 +189,7 @@ class EventDispatcher
return false;
} else {
$q_o = ZMUtil::getModInstance($q_c);
+ $q_o->_running_annotation = $v;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$this->store = $q_o->$q_f(...$params);
$this->status = self::STATUS_NORMAL;
diff --git a/src/ZM/Event/ServerEventHandler.php b/src/ZM/Event/ServerEventHandler.php
index 1167e61c..c11cce95 100644
--- a/src/ZM/Event/ServerEventHandler.php
+++ b/src/ZM/Event/ServerEventHandler.php
@@ -17,13 +17,13 @@ use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Event;
use Swoole\Process;
+use Swoole\Timer;
use Throwable;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
-use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent;
@@ -49,9 +49,9 @@ use ZM\Store\LightCache;
use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool;
-use ZM\Store\WorkerCache;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
+use ZM\Utils\ProcessManager;
use ZM\Utils\ZMUtil;
class ServerEventHandler
@@ -83,7 +83,7 @@ class ServerEventHandler
Process::signal(SIGINT, function () use ($r) {
if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0);
- ZMUtil::reload();
+ \server()->reload();
} else {
echo "\r";
Console::warning("Server interrupted(SIGINT) on Master.");
@@ -133,22 +133,31 @@ class ServerEventHandler
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
LightCache::savePersistence();
}
- Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
+ Console::verbose(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
}
/**
* @SwooleHandler("WorkerStart")
* @param Server $server
* @param $worker_id
+ * @throws Exception
*/
public function onWorkerStart(Server $server, $worker_id) {
//if (ZMBuf::atomic("stop_signal")->get() != 0) return;
+
Process::signal(SIGINT, function () use ($worker_id, $server) {
+ Timer::clearAll();
+ ProcessManager::resumeAllWorkerCoroutines();
Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid());
server()->stop($worker_id);
});
unset(Context::$context[Coroutine::getCid()]);
if ($server->taskworker === false) {
+ Process::signal(SIGUSR1, function() use ($worker_id){
+ Timer::clearAll();
+ ProcessManager::resumeAllWorkerCoroutines();
+ });
+ zm_atomic("_#worker_".$worker_id)->set($server->worker_pid);
try {
register_shutdown_function(function () use ($server) {
$error = error_get_last();
@@ -456,8 +465,12 @@ class ServerEventHandler
}
});
try {
- if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) {
- if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) {
+ $obb_onebot = ZMConfig::get("global", "onebot") ??
+ ZMConfig::get("global", "modules")["onebot"] ??
+ ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
+ $onebot_status = $obb_onebot["status"];
+ if ($conn->getName() === 'qq' && $onebot_status === true) {
+ if ($obb_onebot["single_bot_mode"]) {
LightCacheInside::set("connect", "conn_fd", $request->fd);
}
}
@@ -472,7 +485,6 @@ class ServerEventHandler
Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::trace();
}
- //EventHandler::callSwooleEvent("open", $server, $request);
}
/**
@@ -503,8 +515,11 @@ class ServerEventHandler
}
});
try {
- if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) {
- if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) {
+ $obb_onebot = ZMConfig::get("global", "onebot") ??
+ ZMConfig::get("global", "modules")["onebot"] ??
+ ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
+ if ($conn->getName() === 'qq' && $obb_onebot["status"] === true) {
+ if ($obb_onebot["single_bot_mode"]) {
LightCacheInside::set("connect", "conn_fd", -1);
}
}
@@ -528,70 +543,27 @@ class ServerEventHandler
* @param $src_worker_id
* @param $data
* @throws Exception
+ * @noinspection PhpUnusedParameterInspection
*/
public function onPipeMessage(Server $server, $src_worker_id, $data) {
//var_dump($data, $server->worker_id);
//unset(Context::$context[Co::getCid()]);
$data = json_decode($data, true);
- switch ($data["action"] ?? '') {
- case "resume_ws_message":
- $obj = $data["data"];
- Co::resume($obj["coroutine"]);
- break;
- case "getWorkerCache":
- $r = WorkerCache::get($data["key"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "setWorkerCache":
- $r = WorkerCache::set($data["key"], $data["value"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "unsetWorkerCache":
- $r = WorkerCache::unset($data["key"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "hasKeyWorkerCache":
- $r = WorkerCache::hasKey($data["key"], $data["subkey"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "asyncAddWorkerCache":
- WorkerCache::add($data["key"], $data["value"], true);
- break;
- case "asyncSubWorkerCache":
- WorkerCache::sub($data["key"], $data["value"], true);
- break;
- case "asyncSetWorkerCache":
- WorkerCache::set($data["key"], $data["value"], true);
- break;
- case "asyncUnsetWorkerCache":
- WorkerCache::unset($data["key"], true);
- break;
- case "addWorkerCache":
- $r = WorkerCache::add($data["key"], $data["value"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "subWorkerCache":
- $r = WorkerCache::sub($data["key"], $data["value"]);
- $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
- $server->sendMessage(json_encode($action, 256), $src_worker_id);
- break;
- case "returnWorkerCache":
- WorkerCache::$transfer[$data["cid"]] = $data["value"];
- zm_resume($data["cid"]);
- break;
- default:
- $dispatcher = new EventDispatcher(OnPipeMessageEvent::class);
- $dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) {
- return $v->action == $data["action"];
- });
- $dispatcher->dispatchEvents($data);
- break;
+ ProcessManager::workerAction($src_worker_id, $data);
+ }
+
+ /**
+ * @SwooleHandler("beforeReload")
+ */
+ public function onBeforeReload() {
+ for($i = 0; $i < ZM_WORKER_NUM; ++$i) {
+ $pid = zm_atomic("_#worker_".$i)->get();
+ Process::kill($pid, SIGUSR1);
}
+
+ Console::info(Console::setColor("Reloading server...", "gold"));
+ usleep(800 * 1000);
+ LightCacheInside::unset("wait_api", "wait_api");
}
/**
@@ -693,18 +665,19 @@ class ServerEventHandler
}
//加载插件
- $plugins = ZMConfig::get("global", "modules") ?? [];
- if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
+ $obb_onebot = ZMConfig::get("global", "onebot") ??
+ ZMConfig::get("global", "modules")["onebot"] ??
+ ["status" => true, "single_bot_mode" => false, "message_level" => 99999];
- if ($plugins["onebot"]) {
+ if ($obb_onebot["status"]) {
$obj = new OnSwooleEvent();
$obj->class = QQBot::class;
- $obj->method = 'handle';
+ $obj->method = 'handleByEvent';
$obj->type = 'message';
- $obj->level = $plugins["onebot"]["message_level"] ?? 99999;
+ $obj->level = $obb_onebot["message_level"] ?? 99999;
$obj->rule = 'connectIsQQ()';
EventManager::addEvent(OnSwooleEvent::class, $obj);
- if ($plugins["onebot"]["single_bot_mode"]) {
+ if ($obb_onebot["single_bot_mode"]) {
LightCacheInside::set("connect", "conn_fd", -1);
} else {
LightCacheInside::set("connect", "conn_fd", -2);
diff --git a/src/ZM/Framework.php b/src/ZM/Framework.php
index c164aeb0..5e478345 100644
--- a/src/ZM/Framework.php
+++ b/src/ZM/Framework.php
@@ -5,7 +5,9 @@ namespace ZM;
use Doctrine\Common\Annotations\AnnotationReader;
+use Error;
use Exception;
+use Swoole\Server\Port;
use ZM\Annotation\Swoole\OnSetup;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
@@ -23,6 +25,7 @@ use Swoole\Runtime;
use Swoole\WebSocket\Server;
use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Console\Console;
+use ZM\Utils\Terminal;
use ZM\Utils\ZMUtil;
class Framework
@@ -35,11 +38,16 @@ class Framework
* @var Server
*/
public static $server;
+ /**
+ * @var string[]
+ */
+ public static $loaded_files = [];
/**
* @var array|bool|mixed|null
*/
private $server_set;
+ /** @noinspection PhpUnusedParameterInspection */
public function __construct($args = []) {
$tty_width = $this->getTtyWidth();
@@ -54,7 +62,6 @@ class Framework
//定义常量
include_once "global_defines.php";
- ZMAtomic::init();
try {
$sw = ZMConfig::get("global");
if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
@@ -86,18 +93,23 @@ class Framework
date_default_timezone_set($timezone);
$this->server_set = ZMConfig::get("global", "swoole");
- $this->parseCliArgs(self::$argv);
+ $this->server_set["log_level"] = SWOOLE_LOG_DEBUG;
+ $add_port = ZMConfig::get("global", "remote_terminal")["status"] ?? false;
+ $this->parseCliArgs(self::$argv, $add_port);
+ $worker = $this->server_set["worker_num"] ?? swoole_cpu_num();
+ define("ZM_WORKER_NUM", $worker);
+ ZMAtomic::init();
// 打印初始信息
$out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port");
- if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
- else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"];
+ if (!isset($this->server_set["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
+ else $out["worker"] = $this->server_set["worker_num"];
$out["environment"] = $args["env"] === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : "");
if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
- if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) {
- $out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"];
+ if (isset($this->server_set["task_worker_num"])) {
+ $out["task_worker"] = $this->server_set["task_worker_num"];
}
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
$conf = ZMConfig::get("global", "sql_config");
@@ -114,12 +126,88 @@ class Framework
$out["php_version"] = PHP_VERSION;
$out["swoole_version"] = SWOOLE_VERSION;
}
+ if ($add_port) {
+ $conf = ZMConfig::get("global", "remote_terminal");
+ $out["terminal"] = $conf["host"] . ":" . $conf["port"];
+ }
$out["working_dir"] = DataProvider::getWorkingDir();
self::printProps($out, $tty_width, $args["log-theme"] === null);
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
+ if ($add_port) {
+ $conf = ZMConfig::get("global", "remote_terminal") ?? [
+ 'status' => true,
+ 'host' => '127.0.0.1',
+ 'port' => 20002,
+ 'token' => ''
+ ];
+ $welcome_msg = Console::setColor("Welcome! You can use `help` for usage.", "green");
+ /** @var Port $port */
+ $port = self::$server->listen($conf["host"], $conf["port"], SWOOLE_SOCK_TCP);
+ $port->set([
+ 'open_http_protocol' => false
+ ]);
+ $port->on('connect', function (?\Swoole\Server $serv, $fd) use ($port, $welcome_msg, $conf) {
+ ManagerGM::pushConnect($fd, "terminal");
+ $serv->send($fd, file_get_contents(working_dir() . "/config/motd.txt"));
+ if (!empty($conf["token"])) {
+ $serv->send($fd, "Please input token: ");
+ } else {
+ $serv->send($fd, $welcome_msg . "\n>>> ");
+ }
+ });
+
+ $port->on('receive', function ($serv, $fd, $reactor_id, $data) use ($welcome_msg, $conf) {
+ ob_start();
+ try {
+ $arr = LightCacheInside::get("light_array", "input_token") ?? [];
+ if (empty($arr[$fd] ?? '')) {
+ if ($conf["token"] != '') {
+ $token = trim($data);
+ if ($token === $conf["token"]) {
+ SpinLock::transaction("input_token", function () use ($fd, $token) {
+ $arr = LightCacheInside::get("light_array", "input_token");
+ $arr[$fd] = $token;
+ LightCacheInside::set("light_array", "input_token", $arr);
+ });
+ $serv->send($fd, Console::setColor("Auth success!!\n", "green"));
+ $serv->send($fd, $welcome_msg . "\n>>> ");
+ } else {
+ $serv->send($fd, Console::setColor("Auth failed!!\n", "red"));
+ $serv->close($fd);
+ }
+ return;
+ }
+ }
+ if (trim($data) == "exit" || trim($data) == "q") {
+ $serv->send($fd, Console::setColor("Bye!\n", "blue"));
+ $serv->close($fd);
+ return;
+ }
+ Terminal::executeCommand(trim($data));
+ } catch (Exception $e) {
+ $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
+ Console::error("Uncaught exception " . get_class($e) . " when calling \"open\": " . $error_msg);
+ Console::trace();
+ } catch (Error $e) {
+ $error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
+ Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg);
+ Console::trace();
+ }
+
+ $r = ob_get_clean();
+ if (!empty($r)) $serv->send($fd, $r);
+ if (!in_array(trim($data), ['r', 'reload', 'stop'])) $serv->send($fd, ">>> ");
+ });
+
+ $port->on('close', function ($serv, $fd) {
+ ManagerGM::popConnect($fd);
+ //echo "Client: Close.\n";
+ });
+ }
+
self::$server->set($this->server_set);
Console::setServer(self::$server);
self::printMotd($tty_width);
@@ -195,9 +283,9 @@ class Framework
}
public function start() {
+ self::$loaded_files = get_included_files();
self::$server->start();
zm_atomic("server_is_stopped")->set(1);
- Console::setLevel(0);
}
/**
@@ -243,14 +331,29 @@ class Framework
/**
* 解析命令行的 $argv 参数们
* @param $args
- * @throws Exception
+ * @param $add_port
*/
- private function parseCliArgs($args) {
+ private function parseCliArgs($args, &$add_port) {
$coroutine_mode = true;
global $terminal_id;
$terminal_id = uuidgen();
foreach ($args as $x => $y) {
switch ($x) {
+ case 'worker-num':
+ if (intval($y) >= 1 && intval($y) <= 1024) {
+ $this->server_set["worker_num"] = intval($y);
+ } else {
+ Console::warning("Invalid worker num! Turn to default value (".($this->server_set["worker_num"] ?? swoole_cpu_num()).")");
+ }
+ break;
+ case 'task-worker-num':
+ if (intval($y) >= 1 && intval($y) <= 1024) {
+ $this->server_set["task_worker_num"] = intval($y);
+ $this->server_set["task_enable_coroutine"] = true;
+ } else {
+ Console::warning("Invalid worker num! Turn to default value (0)");
+ }
+ break;
case 'disable-coroutine':
if ($y) {
$coroutine_mode = false;
@@ -296,6 +399,9 @@ class Framework
Console::$theme = $y;
}
break;
+ case 'remote-terminal':
+ $add_port = true;
+ break;
case 'show-php-ver':
default:
//Console::info("Calculating ".$x);
@@ -304,6 +410,7 @@ class Framework
}
}
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
+ else Runtime::enableCoroutine(false, SWOOLE_HOOK_ALL);
}
private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) {
diff --git a/src/ZM/Module/QQBot.php b/src/ZM/Module/QQBot.php
index be54c454..1985d435 100644
--- a/src/ZM/Module/QQBot.php
+++ b/src/ZM/Module/QQBot.php
@@ -15,6 +15,8 @@ use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException;
use ZM\Exception\WaitTimeoutException;
use ZM\Utils\CoMessage;
+use ZM\Utils\MessageUtil;
+use ZM\Utils\SingletonTrait;
/**
* Class QQBot
@@ -22,20 +24,28 @@ use ZM\Utils\CoMessage;
*/
class QQBot
{
+ use SingletonTrait;
+
+ public function handleByEvent() {
+ $data = json_decode(context()->getFrame()->data, true);
+ $this->handle($data);
+ }
+
/**
+ * @param $data
+ * @param int $level
* @throws InterruptException
- * @throws Exception
*/
- public function handle() {
+ public function handle($data, $level = 0) {
try {
- $data = json_decode(context()->getFrame()->data, true);
+ if ($level > 10) return;
set_coroutine_params(["data" => $data]);
if (isset($data["post_type"])) {
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL;
- ctx()->setCache("level", 0);
+ ctx()->setCache("level", $level);
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
if ($data["post_type"] != "meta_event") {
- $r = $this->dispatchBeforeEvents($data); // before在这里执行,元事件不执行before为减少不必要的调试日志
+ $r = $this->dispatchBeforeEvents($data, "pre"); // before在这里执行,元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
//Console::warning("最上数据包:".json_encode($data));
@@ -43,6 +53,10 @@ class QQBot
if (isset($data["echo"]) || isset($data["post_type"])) {
if (CoMessage::resumeByWS()) EventDispatcher::interrupt();
}
+ if (($data["post_type"] ?? "meta_event") != "meta_event") {
+ $r = $this->dispatchBeforeEvents($data, "post"); // before在这里执行,元事件不执行before为减少不必要的调试日志
+ if ($r->store === "block") EventDispatcher::interrupt();
+ }
if (isset($data["post_type"])) $this->dispatchEvents($data);
else $this->dispatchAPIResponse($data);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
@@ -52,14 +66,21 @@ class QQBot
/**
* @param $data
+ * @param $time
* @return EventDispatcher
* @throws Exception
*/
- public function dispatchBeforeEvents($data): EventDispatcher {
+ public function dispatchBeforeEvents($data, $time): EventDispatcher {
$before = new EventDispatcher(CQBefore::class);
- $before->setRuleFunction(function ($v) use ($data) {
- return $v->cq_event == $data["post_type"];
- });
+ if ($time === "pre") {
+ $before->setRuleFunction(function($v) use ($data){
+ return $v->level >= 200 && $v->cq_event == $data["post_type"];
+ });
+ } else {
+ $before->setRuleFunction(function($v) use ($data){
+ return $v->level < 200 && $v->cq_event == $data["post_type"];
+ });
+ }
$before->setReturnFunction(function ($result) {
if (!$result) EventDispatcher::interrupt("block");
});
@@ -76,58 +97,20 @@ class QQBot
//Console::warning("最xia数据包:".json_encode($data));
switch ($data["post_type"]) {
case "message":
- $word = explodeMsg(str_replace("\r", "", context()->getMessage()));
- if (empty($word)) $word = [""];
- if (count(explode("\n", $word[0])) >= 2) {
- $enter = explode("\n", context()->getMessage());
- $first = split_explode(" ", array_shift($enter));
- $word = array_merge($first, $enter);
- foreach ($word as $k => $v) {
- $word[$k] = trim($word[$k]);
- }
- }
//分发CQCommand事件
$dispatcher = new EventDispatcher(CQCommand::class);
- $dispatcher->setRuleFunction(function (CQCommand $v) use ($word) {
- if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false;
- elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getUserId())) &&
- ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (ctx()->getGroupId() ?? 0))) &&
- ($v->message_type == '' || ($v->message_type != '' && $v->message_type == ctx()->getMessageType()))
- ) {
- if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
- array_shift($word);
- ctx()->setCache("match", $word);
- return true;
- } elseif ($v->start_with != "" && mb_strpos(ctx()->getMessage(), $v->start_with) === 0) {
- ctx()->setCache("match", [mb_substr(ctx()->getMessage(), mb_strlen($v->start_with))]);
- return true;
- } elseif ($v->end_with != "" && strlen(ctx()->getMessage()) == (strripos(ctx()->getMessage(), $v->end_with) + strlen($v->end_with))) {
- ctx()->setCache("match", [substr(ctx()->getMessage(), 0, strripos(ctx()->getMessage(), $v->end_with))]);
- return true;
- } elseif ($v->keyword != "" && mb_strpos(ctx()->getMessage(), $v->keyword) !== false) {
- ctx()->setCache("match", explode($v->keyword, ctx()->getMessage()));
- return true;
- } elseif ($v->pattern != "") {
- $match = matchArgs($v->pattern, ctx()->getMessage());
- if ($match !== false) {
- ctx()->setCache("match", $match);
- return true;
- }
- } elseif ($v->regex != "") {
- if (preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) {
- ctx()->setCache("match", $word2);
- return true;
- }
- }
- }
- return false;
- });
$dispatcher->setReturnFunction(function ($result) {
if (is_string($result)) ctx()->reply($result);
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
});
- $dispatcher->dispatchEvents();
- if ($dispatcher->status == EventDispatcher::STATUS_INTERRUPTED) EventDispatcher::interrupt();
+ zm_dump(ctx()->getData());
+ $s = MessageUtil::matchCommand(ctx()->getMessage(), ctx()->getData());
+ if ($s->status !== false) {
+ if (!empty($s->match)) ctx()->setCache("match", $s->match);
+ $dispatcher->dispatchEvent($s->object, null);
+ if (is_string($dispatcher->store)) ctx()->reply($dispatcher->store);
+ if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
+ }
//分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class);
diff --git a/src/ZM/Store/LightCache.php b/src/ZM/Store/LightCache.php
index 060cc0cf..b88ca3dd 100644
--- a/src/ZM/Store/LightCache.php
+++ b/src/ZM/Store/LightCache.php
@@ -7,7 +7,6 @@ namespace ZM\Store;
use Exception;
use Swoole\Table;
use ZM\Annotation\Swoole\OnSave;
-use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Exception\ZMException;
@@ -182,17 +181,12 @@ class LightCache
}
/**
- * @param false $only_worker
+ * 这个只能在唯一一个工作进程中执行
* @throws Exception
*/
- public static function savePersistence($only_worker = false) {
- // 下面将OnSave激活一下
- if (server()->worker_id == (ZMConfig::get("global", "worker_cache")["worker"] ?? 0)) {
- $dispatcher = new EventDispatcher(OnSave::class);
- $dispatcher->dispatchEvents();
- }
-
- if($only_worker) return;
+ public static function savePersistence() {
+ $dispatcher = new EventDispatcher(OnSave::class);
+ $dispatcher->dispatchEvents();
if (self::$kv_table === null) return;
$r = [];
@@ -207,8 +201,7 @@ class LightCache
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
}
-
-
+ Console::verbose("Saved.");
}
private static function checkExpire($key) {
diff --git a/src/ZM/Store/LightCacheInside.php b/src/ZM/Store/LightCacheInside.php
index b9bcc81a..92c3ab38 100644
--- a/src/ZM/Store/LightCacheInside.php
+++ b/src/ZM/Store/LightCacheInside.php
@@ -18,6 +18,7 @@ class LightCacheInside
self::createTable("wait_api", 3, 65536);
self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的
self::createTable("static_route", 64, 256);//用于存储
+ self::createTable("light_array", 8, 512, 0.6);
} catch (ZMException $e) {
return false;
} //用于存协程等待的状态内容的
@@ -59,10 +60,11 @@ class LightCacheInside
* @param $name
* @param $size
* @param $str_size
+ * @param int $conflict_proportion
* @throws ZMException
*/
- private static function createTable($name, $size, $str_size) {
- self::$kv_table[$name] = new Table($size, 0);
+ private static function createTable($name, $size, $str_size, $conflict_proportion = 0) {
+ self::$kv_table[$name] = new Table($size, $conflict_proportion);
self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size);
$r = self::$kv_table[$name]->create();
if ($r === false) throw new ZMException("内存不足,创建静态表失败!");
diff --git a/src/ZM/Store/ZMAtomic.php b/src/ZM/Store/ZMAtomic.php
index c2cb72a2..9955f9f8 100644
--- a/src/ZM/Store/ZMAtomic.php
+++ b/src/ZM/Store/ZMAtomic.php
@@ -32,6 +32,10 @@ class ZMAtomic
self::$atomics["wait_msg_id"] = new Atomic(0);
self::$atomics["_event_id"] = new Atomic(0);
self::$atomics["server_is_stopped"] = new Atomic(0);
+ for($i = 0; $i < ZM_WORKER_NUM; ++$i) {
+ self::$atomics["_#worker_".$i] = new Atomic(0);
+ }
+ echo ("初始化工作进程数量:".ZM_WORKER_NUM.PHP_EOL);
for ($i = 0; $i < 10; ++$i) {
self::$atomics["_tmp_" . $i] = new Atomic(0);
}
diff --git a/src/ZM/Utils/MessageUtil.php b/src/ZM/Utils/MessageUtil.php
index 1396d703..0c11814f 100644
--- a/src/ZM/Utils/MessageUtil.php
+++ b/src/ZM/Utils/MessageUtil.php
@@ -4,9 +4,12 @@
namespace ZM\Utils;
+use ZM\Annotation\CQ\CQCommand;
use ZM\API\CQ;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
+use ZM\Entity\MatchResult;
+use ZM\Event\EventManager;
use ZM\Requests\ZMRequest;
class MessageUtil
@@ -55,6 +58,10 @@ class MessageUtil
return false;
}
+ public static function isAtMe($msg, $me_id) {
+ return strpos($msg, CQ::at($me_id)) !== false;
+ }
+
/**
* 通过本地地址返回图片的 CQ 码
* type == 0 : 返回图片的 base64 CQ 码
@@ -76,4 +83,80 @@ class MessageUtil
}
return "";
}
+
+ /**
+ * 分割字符,将用户消息通过空格或换行分割为数组
+ * @param $msg
+ * @return array|string[]
+ */
+ public static function splitCommand($msg) {
+ $word = explodeMsg(str_replace("\r", "", $msg));
+ if (empty($word)) $word = [""];
+ if (count(explode("\n", $word[0])) >= 2) {
+ $enter = explode("\n", $msg);
+ $first = split_explode(" ", array_shift($enter));
+ $word = array_merge($first, $enter);
+ foreach ($word as $k => $v) {
+ $word[$k] = trim($word[$k]);
+ }
+ }
+ return $word;
+ }
+
+ /**
+ * @param $msg
+ * @param $obj
+ * @return MatchResult
+ */
+ public static function matchCommand($msg, $obj) {
+ $ls = EventManager::$events[CQCommand::class] ?? [];
+ $word = self::splitCommand($msg);
+ $matched = new MatchResult();
+ foreach ($ls as $k => $v) {
+ if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) continue;
+ elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == $obj["user_id"])) &&
+ ($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($obj["group_id"] ?? 0))) &&
+ ($v->message_type == '' || ($v->message_type != '' && $v->message_type == $obj["message_type"]))
+ ) {
+ if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
+ array_shift($word);
+ $matched->match = $word;
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ } elseif ($v->start_with != "" && mb_substr($msg, 0, mb_strlen($v->start_with)) === $v->start_with) {
+ $matched->match = [mb_substr($msg, mb_strlen($v->start_with))];
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ } elseif ($v->end_with != "" && mb_substr($msg, 0 - mb_strlen($v->end_with)) === $v->end_with) {
+ $matched->match = [substr($msg, 0, strripos($msg, $v->end_with))];
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ } elseif ($v->keyword != "" && mb_strpos($msg, $v->keyword) !== false) {
+ $matched->match = explode($v->keyword, $msg);
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ } elseif ($v->pattern != "") {
+ $match = matchArgs($v->pattern, $msg);
+ if ($match !== false) {
+ $matched->match = $match;
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ }
+ } elseif ($v->regex != "") {
+ if (preg_match("/" . $v->regex . "/u", $msg, $word2) != 0) {
+ $matched->match = $word2;
+ $matched->object = $v;
+ $matched->status = true;
+ break;
+ }
+ }
+ }
+ }
+ return $matched;
+ }
}
\ No newline at end of file
diff --git a/src/ZM/Utils/ProcessManager.php b/src/ZM/Utils/ProcessManager.php
index 83b65ac5..973f3d43 100644
--- a/src/ZM/Utils/ProcessManager.php
+++ b/src/ZM/Utils/ProcessManager.php
@@ -4,14 +4,110 @@
namespace ZM\Utils;
+use Co;
+use ZM\Annotation\Swoole\OnPipeMessageEvent;
+use ZM\Console\Console;
+use ZM\Event\EventDispatcher;
+use ZM\Store\LightCache;
+use ZM\Store\LightCacheInside;
+use ZM\Store\WorkerCache;
+
class ProcessManager
{
- public static function runOnTask($param, $timeout = 0.5, $dst_worker_id = -1) {
- return server()->taskwait([
- "action" => "runMethod",
- "class" => $param["class"],
- "method" => $param["method"],
- "params" => $param["params"]
- ], $timeout, $dst_worker_id);
+ public static function workerAction($src_worker_id, $data) {
+ $server = server();
+ switch ($data["action"] ?? '') {
+ case "eval":
+ eval($data["data"]);
+ break;
+ case "call_static":
+ call_user_func_array([$data["data"]["class"], $data["data"]["method"]], $data["data"]["params"]);
+ break;
+ case "save_persistence":
+ LightCache::savePersistence();
+ break;
+ case "resume_ws_message":
+ $obj = $data["data"];
+ Co::resume($obj["coroutine"]);
+ break;
+ case "getWorkerCache":
+ $r = WorkerCache::get($data["key"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "setWorkerCache":
+ $r = WorkerCache::set($data["key"], $data["value"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "unsetWorkerCache":
+ $r = WorkerCache::unset($data["key"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "hasKeyWorkerCache":
+ $r = WorkerCache::hasKey($data["key"], $data["subkey"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "asyncAddWorkerCache":
+ WorkerCache::add($data["key"], $data["value"], true);
+ break;
+ case "asyncSubWorkerCache":
+ WorkerCache::sub($data["key"], $data["value"], true);
+ break;
+ case "asyncSetWorkerCache":
+ WorkerCache::set($data["key"], $data["value"], true);
+ break;
+ case "asyncUnsetWorkerCache":
+ WorkerCache::unset($data["key"], true);
+ break;
+ case "addWorkerCache":
+ $r = WorkerCache::add($data["key"], $data["value"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "subWorkerCache":
+ $r = WorkerCache::sub($data["key"], $data["value"]);
+ $action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
+ $server->sendMessage(json_encode($action, 256), $src_worker_id);
+ break;
+ case "returnWorkerCache":
+ WorkerCache::$transfer[$data["cid"]] = $data["value"];
+ zm_resume($data["cid"]);
+ break;
+ default:
+ $dispatcher = new EventDispatcher(OnPipeMessageEvent::class);
+ $dispatcher->setRuleFunction(function (OnPipeMessageEvent $v) use ($data) {
+ return $v->action == $data["action"];
+ });
+ $dispatcher->dispatchEvents($data);
+ break;
+ }
+ }
+
+ public static function sendActionToWorker($worker_id, $action, $data) {
+ $obj = ["action" => $action, "data" => $data];
+ if (server()->worker_id === -1 && server()->getManagerPid() != posix_getpid()) {
+ Console::warning("Cannot send worker action from master or manager process!");
+ return;
+ }
+ if (server()->worker_id == $worker_id) {
+ self::workerAction($worker_id, $obj);
+ } else {
+ server()->sendMessage(json_encode($obj), $worker_id);
+ }
+ }
+
+ public static function resumeAllWorkerCoroutines() {
+ if (server()->worker_id === -1) {
+ Console::warning("Cannot call '".__FUNCTION__."' in non-worker process!");
+ return;
+ }
+ foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) {
+ if (($v["result"] ?? false) === null && isset($v["coroutine"], $v["worker_id"])) {
+ if (server()->worker_id == $v["worker_id"]) Co::resume($v["coroutine"]);
+ }
+ }
}
}
\ No newline at end of file
diff --git a/src/ZM/Utils/Terminal.php b/src/ZM/Utils/Terminal.php
index fc01cc82..28d80783 100644
--- a/src/ZM/Utils/Terminal.php
+++ b/src/ZM/Utils/Terminal.php
@@ -6,22 +6,38 @@ namespace ZM\Utils;
use Exception;
use Psy\Shell;
-use Swoole\Event;
+use ZM\Annotation\Command\TerminalCommand;
+use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
+use ZM\Event\EventDispatcher;
+use ZM\Event\EventManager;
use ZM\Framework;
class Terminal
{
/**
* @param string $cmd
- * @param $resource
* @return bool
* @noinspection PhpMissingReturnTypeInspection
* @noinspection PhpUnused
+ * @throws Exception
*/
- public static function executeCommand(string $cmd, $resource) {
+ public static function executeCommand(string $cmd) {
$it = explodeMsg($cmd);
switch ($it[0] ?? '') {
+ case 'help':
+ $help[] = "exit | q:\t断开远程终端";
+ $help[] = "logtest:\t输出所有可以打印的log等级示例消息,用于测试Console";
+ $help[] = "call:\t\t用于执行不需要参数的动态函数,比如 `call \Module\Example\Hello hitokoto`";
+ $help[] = "level:\t\t设置log等级,例如 `level 0|1|2|3|4`";
+ $help[] = "bc:\t\teval执行代码,但输入必须是将代码base64之后的,如 `bc em1faW5mbygn5L2g5aW9Jyk7`";
+ $help[] = "stop:\t\t停止服务器";
+ $help[] = "reload | r:\t热重启用户编写的模块代码";
+ foreach((EventManager::$events[TerminalCommand::class] ?? []) as $v) {
+ $help[]=$v->command.":\t\t".(empty($v->description) ? "<用户自定义指令>" : $v->description);
+ }
+ echo implode("\n", $help) . PHP_EOL;
+ return true;
case 'logtest':
Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)");
Console::error("This is error msg. (0)");
@@ -35,7 +51,8 @@ class Terminal
$class_name = $it[1];
$function_name = $it[2];
$class = new $class_name([]);
- $class->$function_name();
+ $r = $class->$function_name();
+ if (is_string($r)) Console::success($r);
return true;
case 'psysh':
if (Framework::$argv["disable-coroutine"]) {
@@ -43,6 +60,11 @@ class Terminal
} else
Console::error("Only \"--disable-coroutine\" mode can use psysh!!!");
return true;
+ case 'level':
+ $level = intval(is_numeric($it[1] ?? 99) ? ($it[1] ?? 99) : 99);
+ if ($level > 4 || $level < 0) Console::warning("Usage: 'level 0|1|2|3|4'");
+ else Console::setLevel($level) || Console::success("Success!!");
+ break;
case 'bc':
$code = base64_decode($it[1] ?? '', true);
try {
@@ -57,7 +79,6 @@ class Terminal
Console::log($it[2], $it[1]);
return true;
case 'stop':
- Event::del($resource);
ZMUtil::stop();
return false;
case 'reload':
@@ -67,8 +88,35 @@ class Terminal
case '':
return true;
default:
- Console::info("Command not found: " . $cmd);
- return true;
+ $dispatcher = new EventDispatcher(TerminalCommand::class);
+ $dispatcher->setRuleFunction(function ($v) use ($it) {
+ /** @var TerminalCommand $v */
+ return $v->command == $it[0];
+ });
+ $dispatcher->setReturnFunction(function () {
+ EventDispatcher::interrupt('none');
+ });
+ $dispatcher->dispatchEvents($it);
+ if ($dispatcher->store !== 'none') {
+ Console::info("Command not found: " . $cmd);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static function log($type, $log_msg) {
+ ob_start();
+ if (!in_array($type, ["log", "info", "debug", "success", "warning", "error", "verbose"])) {
+ ob_get_clean();
+ return;
+ }
+ Console::$type($log_msg);
+ $r = ob_get_clean();
+ $all = ManagerGM::getAllByName("terminal");
+ foreach ($all as $k => $v) {
+ server()->send($v->getFd(), "\r" . $r);
+ server()->send($v->getFd(), ">>> ");
}
}
}
diff --git a/src/ZM/Utils/ZMUtil.php b/src/ZM/Utils/ZMUtil.php
index 0efc014b..30057fd6 100644
--- a/src/ZM/Utils/ZMUtil.php
+++ b/src/ZM/Utils/ZMUtil.php
@@ -4,13 +4,10 @@
namespace ZM\Utils;
-use Co;
use Exception;
-use Swoole\Event;
-use Swoole\Timer;
+use Swoole\Process;
use ZM\Console\Console;
-use ZM\Store\LightCache;
-use ZM\Store\LightCacheInside;
+use ZM\Framework;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf;
@@ -24,39 +21,21 @@ class ZMUtil
if (SpinLock::tryLock("_stop_signal") === false) return;
Console::warning(Console::setColor("Stopping server...", "red"));
if (Console::getLevel() >= 4) Console::trace();
- LightCache::savePersistence();
- if (ZMBuf::$terminal !== null)
- Event::del(ZMBuf::$terminal);
ZMAtomic::get("stop_signal")->set(1);
- try {
- LightCache::set('stop', 'OK');
- } catch (Exception $e) {
+ for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
+ if (Process::kill(zm_atomic("_#worker_" . $i)->get(), 0))
+ Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1);
}
server()->shutdown();
server()->stop();
}
/**
- * @param int $delay
* @throws Exception
*/
- public static function reload($delay = 800) {
- if (server()->worker_id !== -1) {
- Console::info(server()->worker_id);
- zm_atomic("_int_is_reload")->set(1);
- system("kill -INT " . intval(server()->master_pid));
- return;
- }
- Console::info(Console::setColor("Reloading server...", "gold"));
- usleep($delay * 1000);
- foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) {
- if (($v["result"] ?? false) === null && isset($v["coroutine"])) Co::resume($v["coroutine"]);
- }
- LightCacheInside::unset("wait_api", "wait_api");
- LightCache::savePersistence();
- //DataProvider::saveBuffer();
- Timer::clearAll();
- server()->reload();
+ public static function reload() {
+ zm_atomic("_int_is_reload")->set(1);
+ system("kill -INT " . intval(server()->master_pid));
}
public static function getModInstance($class) {
@@ -69,6 +48,22 @@ class ZMUtil
}
public static function sendActionToWorker($target_id, $action, $data) {
+ Console::verbose($action . ": " . $data);
server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id);
}
+
+ /**
+ * 在工作进程中返回可以通过reload重新加载的php文件列表
+ * @return string[]|string[][]
+ */
+ public static function getReloadableFiles() {
+ return array_map(
+ function ($x) {
+ return str_replace(DataProvider::getWorkingDir() . "/", "", $x);
+ }, array_diff(
+ get_included_files(),
+ Framework::$loaded_files
+ )
+ );
+ }
}
diff --git a/src/ZM/global_functions.php b/src/ZM/global_functions.php
index 14e790dd..9b38f3cf 100644
--- a/src/ZM/global_functions.php
+++ b/src/ZM/global_functions.php
@@ -3,6 +3,7 @@
use Swoole\Atomic;
use Swoole\Coroutine;
use Swoole\WebSocket\Server;
+use Symfony\Component\VarDumper\VarDumper;
use ZM\API\ZMRobot;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
@@ -243,8 +244,6 @@ function ctx(): ?ContextInterface {
}
}
-function zm_debug($msg) { Console::debug($msg); }
-
function onebot_target_id_name($message_type): string {
return ($message_type == "group" ? "group_id" : "user_id");
}
@@ -255,13 +254,21 @@ function zm_sleep($s = 1): bool {
return true;
}
-function zm_exec($cmd): array { return System::exec($cmd); }
+function zm_exec($cmd): array {
+ return System::exec($cmd);
+}
-function zm_cid() { return Co::getCid(); }
+function zm_cid() {
+ return Co::getCid();
+}
-function zm_yield() { Co::yield(); }
+function zm_yield() {
+ Co::yield();
+}
-function zm_resume(int $cid) { Co::resume($cid); }
+function zm_resume(int $cid) {
+ Co::resume($cid);
+}
function zm_timer_after($ms, callable $callable) {
Swoole\Timer::after($ms, function () use ($callable) {
@@ -287,10 +294,14 @@ function zm_timer_tick($ms, callable $callable) {
if (zm_cid() === -1) {
return go(function () use ($ms, $callable) {
Console::debug("Adding extra timer tick of " . $ms . " ms");
- Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
+ Swoole\Timer::tick($ms, function () use ($callable) {
+ call_with_catch($callable);
+ });
});
} else {
- return Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
+ return Swoole\Timer::tick($ms, function () use ($callable) {
+ call_with_catch($callable);
+ });
}
}
@@ -354,3 +365,32 @@ function working_dir() {
elseif (LOAD_MODE == 2) return realpath('.');
return null;
}
+
+/** @noinspection PhpMissingReturnTypeInspection */
+function zm_dump($var, ...$moreVars) {
+ VarDumper::dump($var);
+
+ foreach ($moreVars as $v) {
+ VarDumper::dump($v);
+ }
+
+ if (1 < func_num_args()) {
+ return func_get_args();
+ }
+
+ return $var;
+}
+
+function zm_info($obj) { Console::info($obj); }
+
+function zm_warning($obj) { Console::warning($obj); }
+
+function zm_success($obj) { Console::success($obj); }
+
+function zm_debug($obj) { Console::debug($obj); }
+
+function zm_verbose($obj) { Console::verbose($obj); }
+
+function zm_error($obj) { Console::error($obj); }
+
+function zm_config($name, $key = null) { return ZMConfig::get($name, $key); }