update to v2.4.0 (build 399)

add CheckConfigCommand.php
add config update record docs
adjust swoole version to 4.5.0
fix stop and reload bugs
add $_running_annotation
add remote terminal
update global config
add timer tick exception handler
add zm_xxx global functions
add isAtMe(), splitCommand(), matchCommand() function for MessageUtil
add workerAction(), sendActionToWorker(), resumeAllWorkerCoroutines() functions for ProcessManager
optimize CQCommand match function
add custom TerminalCommand annotation
add TuringAPI
add getReloadableFiles() function for ZMUtil
This commit is contained in:
jerry 2021-03-24 23:34:46 +08:00
parent 28f7f20728
commit 6155236d3c
39 changed files with 1244 additions and 252 deletions

View File

@ -1,3 +0,0 @@
FROM zmbot/swoole:latest
# TODO: auto-setup entrypoint

View File

@ -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.

View File

@ -39,7 +39,7 @@ $config['light_cache'] = [
'size' => 512, //最多允许储存的条数需要2的倍数 'size' => 512, //最多允许储存的条数需要2的倍数
'max_strlen' => 32768, //单行字符串最大长度需要2的倍数 'max_strlen' => 32768, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多 'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json', 'persistence_path' => $config['zm_data'] . '_cache.json',
'auto_save_interval' => 900 'auto_save_interval' => 900
]; ];
@ -112,22 +112,19 @@ $config['server_event_handler_class'] = [
// 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类 // 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类
]; ];
/** 服务器启用的外部第三方和内部插件 */ /** 机器人解析模块关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
$config['modules'] = [ $config['onebot'] = [
'onebot' => [ // 机器人解析模块,关闭后无法使用如@CQCommand等注解 'status' => true,
'status' => true, 'single_bot_mode' => false,
'single_bot_mode' => false 'message_level' => 99999
], ];
'http_proxy_server' => [ // 一个内置的简单HTTP代理服务器目前还没有认证功能预计2.4.0版本完成
'status' => false, /** 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */
'host' => '0.0.0.0', $config['remote_terminal'] = [
'port' => 8083, 'status' => false,
'swoole_set_override' => [ 'host' => '127.0.0.1',
'backlog' => 128, 'port' => 20002,
'buffer_output_size' => 1024 * 1024 * 128, 'token' => ''
'socket_buffer_size' => 1024 * 1024 * 1
]
],
]; ];
return $config; return $config;

View File

@ -1,3 +1,23 @@
# FAQ # FAQ
这里会写一些常见的疑难解答。 这里会写一些常见的疑难解答。
## 启动时报错 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 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死
```

View File

@ -27,3 +27,44 @@
TODO先放一放。 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 ==> [
"我叫顺溜",
"我今年二十八"
]
*/
```

View File

@ -0,0 +1,3 @@
# 使用 TaskWorker 进程处理密集运算
> 新开个坑有时间补上。__填坑标记__

View File

@ -134,10 +134,6 @@ set_coroutine_params(["data" => [
别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。 别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。
## zm_debug()
`Console::debug($msg)`
## zm_sleep() ## zm_sleep()
协程版 `sleep()` 函数。 协程版 `sleep()` 函数。
@ -256,3 +252,61 @@ bot()->sendPrivateMsg(123456, "你好啊!!");
定义:`getAllFdByConnectType(string $type = 'default'): array` 定义:`getAllFdByConnectType(string $type = 'default'): array`
`$type``qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。 `$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);
```
<img src="../assets/img/image-20210321193956832.png" alt="image-20210321193956832" style="zoom:50%;" />
## 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)`

View File

@ -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` 对象,含有匹配成功与否,匹配到的注解对象,匹配到的分割词等,见 []

View File

@ -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)。

View File

@ -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");
```

View File

@ -21,3 +21,47 @@ class ASD{
ZMUtil::getModInstance(ASD::class)->test = 5; 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)。

View File

@ -262,6 +262,27 @@
无。 无。
## TerminalCommand()
添加一个远程终端的自定义命令。2.4.0 版本起可用)
### 属性
| 类型 | 值 |
| ---------- | --------------------------------------- |
| 名称 | `@TerminalCommand` |
| 触发前提 | 连接到远程终端可触发 |
| 命名空间 | `ZM\Annotation\Command\TerminalCommand` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 默认 |
| ----------- | ------------------------------ | ---- |
| command | `string`**必填**,命令字符串 | |
| description | `string`,要显示的帮助文本 | 空 |
## 示例1机器人连接框架后输出信息 ## 示例1机器人连接框架后输出信息
```php ```php
@ -406,3 +427,6 @@ public function onCrawl() {
} }
``` ```
## 示例6创建一个远程终端命令并调试框架
> 开个坑以后填。__填坑标记__

25
docs/update/config.md Normal file
View File

@ -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' => ''
];
```

View File

@ -1,5 +1,39 @@
# 更新日志v2 版本) # 更新日志v2 版本)
## v2.4.0build 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) ## v2.3.5 (build 398)
> 更新时间2021.3.23 > 更新时间2021.3.23
@ -15,7 +49,6 @@
- 规范代码,修复一个小报错的 bug - 规范代码,修复一个小报错的 bug
## v2.3.0 ## v2.3.0
> 更新时间2021.3.16 > 更新时间2021.3.16

View File

@ -72,9 +72,12 @@ nav:
- 事件分发器: event/event-dispatcher.md - 事件分发器: event/event-dispatcher.md
- 框架组件: - 框架组件:
- 框架组件: component/index.md - 框架组件: component/index.md
- 机器人 API: component/robot-api.md
- CQ 码(多媒体消息): component/cqcode.md
- 上下文: component/context.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 - LightCache 轻量缓存: component/light-cache.md
- MySQL 数据库: component/mysql.md - MySQL 数据库: component/mysql.md
@ -82,13 +85,15 @@ nav:
- ZMAtomic 原子计数器: component/atomics.md - ZMAtomic 原子计数器: component/atomics.md
- SpinLock 自旋锁: component/spin-lock.md - SpinLock 自旋锁: component/spin-lock.md
- 文件管理: component/data-provider.md - 文件管理: component/data-provider.md
- HTTP 服务器工具类:
- HTTP 和 WebSocket 客户端: component/zmrequest.md
- HTTP 路由管理: component/route-manager.md
- 协程池: component/coroutine-pool.md - 协程池: component/coroutine-pool.md
- 单例类: component/singleton-trait.md - 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md - ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md - 全局方法: component/global-functions.md
- HTTP 和 WebSocket 客户端: component/zmrequest.md
- Console 终端: component/console.md - Console 终端: component/console.md
- Token 验证: component/access-token.md - TaskWorker 管理: component/task-worker.md
- 进阶开发: - 进阶开发:
- 进阶开发: advanced/index.md - 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md - 框架剖析: advanced/framework-structure.md
@ -97,6 +102,7 @@ nav:
- 内部类文件手册: advanced/inside-class.md - 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md - 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md - 框架多进程: advanced/multi-process.md
- TaskWorker 提高并发: advanced/task-worker.md
- 开发实战教程: - 开发实战教程:
- 编写管理员才能触发的功能: advanced/example/admin.md - 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md - FAQ: FAQ.md

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@ -2,17 +2,23 @@
namespace Module\Example; namespace Module\Example;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQMessage;
use ZM\Annotation\Http\Middleware; use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent; use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\API\CQ;
use ZM\API\TuringAPI;
use ZM\ConnectionManager\ConnectionObject; use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand; use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Http\RequestMapping;
use ZM\Event\EventDispatcher; use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException; use ZM\Exception\InterruptException;
use ZM\Module\QQBot;
use ZM\Requests\ZMRequest; use ZM\Requests\ZMRequest;
use ZM\Utils\MessageUtil;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
/** /**
@ -67,6 +73,42 @@ class Hello
return $obj["hitokoto"] . "\n----「" . $obj["from"] . ""; 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 * 一个简单随机数的功能demo
* 问法1随机数 1 20 * 问法1随机数 1 20

118
src/ZM/API/TuringAPI.php Normal file
View File

@ -0,0 +1,118 @@
<?php
namespace ZM\API;
use Swoole\Coroutine\Http\Client;
use ZM\Console\Console;
class TuringAPI
{
/**
* 请求图灵API返回图灵的消息
* @param $msg
* @param $user_id
* @param $api
* @return string
*/
public static function getTuringMsg($msg, $user_id, $api) {
$origin = $msg;
if (($cq = CQ::getCQ($msg)) !== null) {//如有CQ码则去除
if ($cq["type"] == "image") {
$url = $cq["params"]["url"];
$msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg);
}
$msg = trim($msg);
}
//构建将要发送的json包给图灵
$content = [
"reqType" => 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;
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace ZM\Annotation\Command;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class TerminalCommand
* @package ZM\Annotation\Command
* @Annotation
* @Target("METHOD")
*/
class TerminalCommand
{
/**
* @var string
* @Required()
*/
public $command;
/**
* @var string
*/
public $description = "";
}

View File

@ -0,0 +1,56 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CheckConfigCommand extends Command
{
protected static $defaultName = 'check-config';
private $need_update = false;
protected function configure() {
$this->setDescription("检查配置文件是否和框架当前版本有更新");
}
/** @noinspection PhpIncludeInspection */
protected function execute(InputInterface $input, OutputInterface $output): int {
if (LOAD_MODE !== 1) {
$output->writeln("<error>仅限在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("<comment>有配置文件需要更新,详情见文档 https://framework.zhamao.xin/update/config.md</comment>");
}
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("<info>配置文件 ".$local . " 需要更新!</info>");
$this->need_update = true;
return;
}
}
}
}

View File

@ -18,7 +18,7 @@ class DaemonStopCommand extends DaemonCommand
protected function execute(InputInterface $input, OutputInterface $output): int { protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output); 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"); unlink(DataProvider::getWorkingDir() . "/.daemon_pid");
$output->writeln("<info>成功停止!</info>"); $output->writeln("<info>成功停止!</info>");
return Command::SUCCESS; return Command::SUCCESS;

View File

@ -23,8 +23,11 @@ class RunServerCommand extends Command
new InputOption("log-error", null, null, "调整消息等级到error (log-level=0)"), new InputOption("log-error", null, null, "调整消息等级到error (log-level=0)"),
new InputOption("log-theme", null, InputOption::VALUE_REQUIRED, "改变终端的主题配色"), new InputOption("log-theme", null, InputOption::VALUE_REQUIRED, "改变终端的主题配色"),
new InputOption("disable-console-input", null, null, "禁止终端输入内容 (废弃)"), new InputOption("disable-console-input", null, null, "禁止终端输入内容 (废弃)"),
new InputOption("remote-terminal", null, null, "启用远程终端配置使用global.php中的"),
new InputOption("disable-coroutine", null, null, "关闭协程Hook"), new InputOption("disable-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"), 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("watch", null, null, "监听 src/ 目录的文件变化并热更新"),
new InputOption("show-php-ver", null, null, "启动时显示PHP和Swoole版本"), new InputOption("show-php-ver", null, null, "启动时显示PHP和Swoole版本"),
new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"), new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"),

View File

@ -5,6 +5,7 @@ namespace ZM;
use Exception; use Exception;
use ZM\Command\CheckConfigCommand;
use ZM\Command\DaemonReloadCommand; use ZM\Command\DaemonReloadCommand;
use ZM\Command\DaemonStatusCommand; use ZM\Command\DaemonStatusCommand;
use ZM\Command\DaemonStopCommand; use ZM\Command\DaemonStopCommand;
@ -18,8 +19,8 @@ use ZM\Utils\DataProvider;
class ConsoleApplication extends Application class ConsoleApplication extends Application
{ {
const VERSION_ID = 398; const VERSION_ID = 399;
const VERSION = "2.3.5"; const VERSION = "2.4.0";
public function __construct(string $name = 'UNKNOWN') { public function __construct(string $name = 'UNKNOWN') {
define("ZM_VERSION_ID", self::VERSION_ID); define("ZM_VERSION_ID", self::VERSION_ID);
@ -75,6 +76,9 @@ class ConsoleApplication extends Application
new InitCommand(), //初始化用的用于项目初始化和phar初始化 new InitCommand(), //初始化用的用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令 new PureHttpCommand() //纯HTTP服务器指令
]); ]);
if (LOAD_MODE === 1) {
$this->add(new CheckConfigCommand());
}
/* /*
$command_register = ZMConfig::get("global", "command_register_class") ?? []; $command_register = ZMConfig::get("global", "command_register_class") ?? [];
foreach ($command_register as $v) { foreach ($command_register as $v) {
@ -100,7 +104,7 @@ class ConsoleApplication extends Application
private function selfCheck(): bool { 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 (!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."); if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required.");
return true; return true;
} }

View File

@ -216,7 +216,7 @@ class Context implements ContextInterface
* @throws WaitTimeoutException * @throws WaitTimeoutException
*/ */
public function getArgs($mode, $prompt_msg) { public function getArgs($mode, $prompt_msg) {
$arg = ctx()->getCache("match"); $arg = ctx()->getCache("match") ?? [];
switch ($mode) { switch ($mode) {
case ZM_MATCH_ALL: case ZM_MATCH_ALL:
$p = $arg; $p = $arg;

View File

@ -0,0 +1,17 @@
<?php
namespace ZM\Entity;
use ZM\Annotation\CQ\CQCommand;
class MatchResult
{
/** @var bool */
public $status = false;
/** @var CQCommand|null */
public $object = null;
/** @var array */
public $match = [];
}

View File

@ -152,6 +152,7 @@ class EventDispatcher
if ($before_result) { if ($before_result) {
try { try {
$q_o = ZMUtil::getModInstance($q_c); $q_o = ZMUtil::getModInstance($q_c);
$q_o->_running_annotation = $v;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$this->store = $q_o->$q_f(...$params); $this->store = $q_o->$q_f(...$params);
} catch (Exception $e) { } catch (Exception $e) {
@ -188,6 +189,7 @@ class EventDispatcher
return false; return false;
} else { } else {
$q_o = ZMUtil::getModInstance($q_c); $q_o = ZMUtil::getModInstance($q_c);
$q_o->_running_annotation = $v;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ..."); if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$this->store = $q_o->$q_f(...$params); $this->store = $q_o->$q_f(...$params);
$this->status = self::STATUS_NORMAL; $this->status = self::STATUS_NORMAL;

View File

@ -17,13 +17,13 @@ use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool; use Swoole\Database\PDOPool;
use Swoole\Event; use Swoole\Event;
use Swoole\Process; use Swoole\Process;
use Swoole\Timer;
use Throwable; use Throwable;
use ZM\Annotation\AnnotationParser; use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping; use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent; use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnMessageEvent; use ZM\Annotation\Swoole\OnMessageEvent;
use ZM\Annotation\Swoole\OnOpenEvent; use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent; use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart; use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent; use ZM\Annotation\Swoole\OnSwooleEvent;
@ -49,9 +49,9 @@ use ZM\Store\LightCache;
use ZM\Store\LightCacheInside; use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage; use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool; use ZM\Store\Redis\ZMRedisPool;
use ZM\Store\WorkerCache;
use ZM\Utils\DataProvider; use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil; use ZM\Utils\HttpUtil;
use ZM\Utils\ProcessManager;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
class ServerEventHandler class ServerEventHandler
@ -83,7 +83,7 @@ class ServerEventHandler
Process::signal(SIGINT, function () use ($r) { Process::signal(SIGINT, function () use ($r) {
if (zm_atomic("_int_is_reload")->get() === 1) { if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0); zm_atomic("_int_is_reload")->set(0);
ZMUtil::reload(); \server()->reload();
} else { } else {
echo "\r"; echo "\r";
Console::warning("Server interrupted(SIGINT) on Master."); Console::warning("Server interrupted(SIGINT) on Master.");
@ -133,22 +133,31 @@ class ServerEventHandler
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) { if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
LightCache::savePersistence(); LightCache::savePersistence();
} }
Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止"); Console::verbose(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
} }
/** /**
* @SwooleHandler("WorkerStart") * @SwooleHandler("WorkerStart")
* @param Server $server * @param Server $server
* @param $worker_id * @param $worker_id
* @throws Exception
*/ */
public function onWorkerStart(Server $server, $worker_id) { public function onWorkerStart(Server $server, $worker_id) {
//if (ZMBuf::atomic("stop_signal")->get() != 0) return; //if (ZMBuf::atomic("stop_signal")->get() != 0) return;
Process::signal(SIGINT, function () use ($worker_id, $server) { 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()); Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid());
server()->stop($worker_id); server()->stop($worker_id);
}); });
unset(Context::$context[Coroutine::getCid()]); unset(Context::$context[Coroutine::getCid()]);
if ($server->taskworker === false) { 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 { try {
register_shutdown_function(function () use ($server) { register_shutdown_function(function () use ($server) {
$error = error_get_last(); $error = error_get_last();
@ -456,8 +465,12 @@ class ServerEventHandler
} }
}); });
try { try {
if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { $obb_onebot = ZMConfig::get("global", "onebot") ??
if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { 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); 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::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::trace(); Console::trace();
} }
//EventHandler::callSwooleEvent("open", $server, $request);
} }
/** /**
@ -503,8 +515,11 @@ class ServerEventHandler
} }
}); });
try { try {
if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) { $obb_onebot = ZMConfig::get("global", "onebot") ??
if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) { 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); LightCacheInside::set("connect", "conn_fd", -1);
} }
} }
@ -528,70 +543,27 @@ class ServerEventHandler
* @param $src_worker_id * @param $src_worker_id
* @param $data * @param $data
* @throws Exception * @throws Exception
* @noinspection PhpUnusedParameterInspection
*/ */
public function onPipeMessage(Server $server, $src_worker_id, $data) { public function onPipeMessage(Server $server, $src_worker_id, $data) {
//var_dump($data, $server->worker_id); //var_dump($data, $server->worker_id);
//unset(Context::$context[Co::getCid()]); //unset(Context::$context[Co::getCid()]);
$data = json_decode($data, true); $data = json_decode($data, true);
switch ($data["action"] ?? '') { ProcessManager::workerAction($src_worker_id, $data);
case "resume_ws_message": }
$obj = $data["data"];
Co::resume($obj["coroutine"]); /**
break; * @SwooleHandler("beforeReload")
case "getWorkerCache": */
$r = WorkerCache::get($data["key"]); public function onBeforeReload() {
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r]; for($i = 0; $i < ZM_WORKER_NUM; ++$i) {
$server->sendMessage(json_encode($action, 256), $src_worker_id); $pid = zm_atomic("_#worker_".$i)->get();
break; Process::kill($pid, SIGUSR1);
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;
} }
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") ?? []; $obb_onebot = ZMConfig::get("global", "onebot") ??
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false, "message_level" => 99999]; 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 = new OnSwooleEvent();
$obj->class = QQBot::class; $obj->class = QQBot::class;
$obj->method = 'handle'; $obj->method = 'handleByEvent';
$obj->type = 'message'; $obj->type = 'message';
$obj->level = $plugins["onebot"]["message_level"] ?? 99999; $obj->level = $obb_onebot["message_level"] ?? 99999;
$obj->rule = 'connectIsQQ()'; $obj->rule = 'connectIsQQ()';
EventManager::addEvent(OnSwooleEvent::class, $obj); EventManager::addEvent(OnSwooleEvent::class, $obj);
if ($plugins["onebot"]["single_bot_mode"]) { if ($obb_onebot["single_bot_mode"]) {
LightCacheInside::set("connect", "conn_fd", -1); LightCacheInside::set("connect", "conn_fd", -1);
} else { } else {
LightCacheInside::set("connect", "conn_fd", -2); LightCacheInside::set("connect", "conn_fd", -2);

View File

@ -5,7 +5,9 @@ namespace ZM;
use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationReader;
use Error;
use Exception; use Exception;
use Swoole\Server\Port;
use ZM\Annotation\Swoole\OnSetup; use ZM\Annotation\Swoole\OnSetup;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM; use ZM\ConnectionManager\ManagerGM;
@ -23,6 +25,7 @@ use Swoole\Runtime;
use Swoole\WebSocket\Server; use Swoole\WebSocket\Server;
use ZM\Annotation\Swoole\SwooleHandler; use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Utils\Terminal;
use ZM\Utils\ZMUtil; use ZM\Utils\ZMUtil;
class Framework class Framework
@ -35,11 +38,16 @@ class Framework
* @var Server * @var Server
*/ */
public static $server; public static $server;
/**
* @var string[]
*/
public static $loaded_files = [];
/** /**
* @var array|bool|mixed|null * @var array|bool|mixed|null
*/ */
private $server_set; private $server_set;
/** @noinspection PhpUnusedParameterInspection */
public function __construct($args = []) { public function __construct($args = []) {
$tty_width = $this->getTtyWidth(); $tty_width = $this->getTtyWidth();
@ -54,7 +62,6 @@ class Framework
//定义常量 //定义常量
include_once "global_defines.php"; include_once "global_defines.php";
ZMAtomic::init();
try { try {
$sw = ZMConfig::get("global"); $sw = ZMConfig::get("global");
if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]); if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
@ -86,18 +93,23 @@ class Framework
date_default_timezone_set($timezone); date_default_timezone_set($timezone);
$this->server_set = ZMConfig::get("global", "swoole"); $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"); $out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port");
if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)"; if (!isset($this->server_set["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"]; else $out["worker"] = $this->server_set["worker_num"];
$out["environment"] = $args["env"] === null ? "default" : $args["env"]; $out["environment"] = $args["env"] === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel(); $out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : ""); $out["version"] = ZM_VERSION . (LOAD_MODE == 0 ? (" (build " . ZM_VERSION_ID . ")") : "");
if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION; if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) { if (isset($this->server_set["task_worker_num"])) {
$out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"]; $out["task_worker"] = $this->server_set["task_worker_num"];
} }
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") { if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
$conf = ZMConfig::get("global", "sql_config"); $conf = ZMConfig::get("global", "sql_config");
@ -114,12 +126,88 @@ class Framework
$out["php_version"] = PHP_VERSION; $out["php_version"] = PHP_VERSION;
$out["swoole_version"] = SWOOLE_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(); $out["working_dir"] = DataProvider::getWorkingDir();
self::printProps($out, $tty_width, $args["log-theme"] === null); self::printProps($out, $tty_width, $args["log-theme"] === null);
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port")); 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); self::$server->set($this->server_set);
Console::setServer(self::$server); Console::setServer(self::$server);
self::printMotd($tty_width); self::printMotd($tty_width);
@ -195,9 +283,9 @@ class Framework
} }
public function start() { public function start() {
self::$loaded_files = get_included_files();
self::$server->start(); self::$server->start();
zm_atomic("server_is_stopped")->set(1); zm_atomic("server_is_stopped")->set(1);
Console::setLevel(0);
} }
/** /**
@ -243,14 +331,29 @@ class Framework
/** /**
* 解析命令行的 $argv 参数们 * 解析命令行的 $argv 参数们
* @param $args * @param $args
* @throws Exception * @param $add_port
*/ */
private function parseCliArgs($args) { private function parseCliArgs($args, &$add_port) {
$coroutine_mode = true; $coroutine_mode = true;
global $terminal_id; global $terminal_id;
$terminal_id = uuidgen(); $terminal_id = uuidgen();
foreach ($args as $x => $y) { foreach ($args as $x => $y) {
switch ($x) { 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': case 'disable-coroutine':
if ($y) { if ($y) {
$coroutine_mode = false; $coroutine_mode = false;
@ -296,6 +399,9 @@ class Framework
Console::$theme = $y; Console::$theme = $y;
} }
break; break;
case 'remote-terminal':
$add_port = true;
break;
case 'show-php-ver': case 'show-php-ver':
default: default:
//Console::info("Calculating ".$x); //Console::info("Calculating ".$x);
@ -304,6 +410,7 @@ class Framework
} }
} }
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); 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) { private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) {

View File

@ -15,6 +15,8 @@ use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException; use ZM\Exception\InterruptException;
use ZM\Exception\WaitTimeoutException; use ZM\Exception\WaitTimeoutException;
use ZM\Utils\CoMessage; use ZM\Utils\CoMessage;
use ZM\Utils\MessageUtil;
use ZM\Utils\SingletonTrait;
/** /**
* Class QQBot * Class QQBot
@ -22,20 +24,28 @@ use ZM\Utils\CoMessage;
*/ */
class QQBot 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 InterruptException
* @throws Exception
*/ */
public function handle() { public function handle($data, $level = 0) {
try { try {
$data = json_decode(context()->getFrame()->data, true); if ($level > 10) return;
set_coroutine_params(["data" => $data]); set_coroutine_params(["data" => $data]);
if (isset($data["post_type"])) { if (isset($data["post_type"])) {
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL; //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()); //Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
if ($data["post_type"] != "meta_event") { 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(); if ($r->store === "block") EventDispatcher::interrupt();
} }
//Console::warning("最上数据包:".json_encode($data)); //Console::warning("最上数据包:".json_encode($data));
@ -43,6 +53,10 @@ class QQBot
if (isset($data["echo"]) || isset($data["post_type"])) { if (isset($data["echo"]) || isset($data["post_type"])) {
if (CoMessage::resumeByWS()) EventDispatcher::interrupt(); 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); if (isset($data["post_type"])) $this->dispatchEvents($data);
else $this->dispatchAPIResponse($data); else $this->dispatchAPIResponse($data);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) { } /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
@ -52,14 +66,21 @@ class QQBot
/** /**
* @param $data * @param $data
* @param $time
* @return EventDispatcher * @return EventDispatcher
* @throws Exception * @throws Exception
*/ */
public function dispatchBeforeEvents($data): EventDispatcher { public function dispatchBeforeEvents($data, $time): EventDispatcher {
$before = new EventDispatcher(CQBefore::class); $before = new EventDispatcher(CQBefore::class);
$before->setRuleFunction(function ($v) use ($data) { if ($time === "pre") {
return $v->cq_event == $data["post_type"]; $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) { $before->setReturnFunction(function ($result) {
if (!$result) EventDispatcher::interrupt("block"); if (!$result) EventDispatcher::interrupt("block");
}); });
@ -76,58 +97,20 @@ class QQBot
//Console::warning("最xia数据包".json_encode($data)); //Console::warning("最xia数据包".json_encode($data));
switch ($data["post_type"]) { switch ($data["post_type"]) {
case "message": 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事件 //分发CQCommand事件
$dispatcher = new EventDispatcher(CQCommand::class); $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) { $dispatcher->setReturnFunction(function ($result) {
if (is_string($result)) ctx()->reply($result); if (is_string($result)) ctx()->reply($result);
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt(); if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
}); });
$dispatcher->dispatchEvents(); zm_dump(ctx()->getData());
if ($dispatcher->status == EventDispatcher::STATUS_INTERRUPTED) EventDispatcher::interrupt(); $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事件 //分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class); $msg_dispatcher = new EventDispatcher(CQMessage::class);

View File

@ -7,7 +7,6 @@ namespace ZM\Store;
use Exception; use Exception;
use Swoole\Table; use Swoole\Table;
use ZM\Annotation\Swoole\OnSave; use ZM\Annotation\Swoole\OnSave;
use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Event\EventDispatcher; use ZM\Event\EventDispatcher;
use ZM\Exception\ZMException; use ZM\Exception\ZMException;
@ -182,17 +181,12 @@ class LightCache
} }
/** /**
* @param false $only_worker * 这个只能在唯一一个工作进程中执行
* @throws Exception * @throws Exception
*/ */
public static function savePersistence($only_worker = false) { public static function savePersistence() {
// 下面将OnSave激活一下 $dispatcher = new EventDispatcher(OnSave::class);
if (server()->worker_id == (ZMConfig::get("global", "worker_cache")["worker"] ?? 0)) { $dispatcher->dispatchEvents();
$dispatcher = new EventDispatcher(OnSave::class);
$dispatcher->dispatchEvents();
}
if($only_worker) return;
if (self::$kv_table === null) return; if (self::$kv_table === null) return;
$r = []; $r = [];
@ -207,8 +201,7 @@ class LightCache
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256)); $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\"!"); if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
} }
Console::verbose("Saved.");
} }
private static function checkExpire($key) { private static function checkExpire($key) {

View File

@ -18,6 +18,7 @@ class LightCacheInside
self::createTable("wait_api", 3, 65536); self::createTable("wait_api", 3, 65536);
self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的 self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的
self::createTable("static_route", 64, 256);//用于存储 self::createTable("static_route", 64, 256);//用于存储
self::createTable("light_array", 8, 512, 0.6);
} catch (ZMException $e) { } catch (ZMException $e) {
return false; return false;
} //用于存协程等待的状态内容的 } //用于存协程等待的状态内容的
@ -59,10 +60,11 @@ class LightCacheInside
* @param $name * @param $name
* @param $size * @param $size
* @param $str_size * @param $str_size
* @param int $conflict_proportion
* @throws ZMException * @throws ZMException
*/ */
private static function createTable($name, $size, $str_size) { private static function createTable($name, $size, $str_size, $conflict_proportion = 0) {
self::$kv_table[$name] = new Table($size, 0); self::$kv_table[$name] = new Table($size, $conflict_proportion);
self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size); self::$kv_table[$name]->column("value", Table::TYPE_STRING, $str_size);
$r = self::$kv_table[$name]->create(); $r = self::$kv_table[$name]->create();
if ($r === false) throw new ZMException("内存不足,创建静态表失败!"); if ($r === false) throw new ZMException("内存不足,创建静态表失败!");

View File

@ -32,6 +32,10 @@ class ZMAtomic
self::$atomics["wait_msg_id"] = new Atomic(0); self::$atomics["wait_msg_id"] = new Atomic(0);
self::$atomics["_event_id"] = new Atomic(0); self::$atomics["_event_id"] = new Atomic(0);
self::$atomics["server_is_stopped"] = 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) { for ($i = 0; $i < 10; ++$i) {
self::$atomics["_tmp_" . $i] = new Atomic(0); self::$atomics["_tmp_" . $i] = new Atomic(0);
} }

View File

@ -4,9 +4,12 @@
namespace ZM\Utils; namespace ZM\Utils;
use ZM\Annotation\CQ\CQCommand;
use ZM\API\CQ; use ZM\API\CQ;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Entity\MatchResult;
use ZM\Event\EventManager;
use ZM\Requests\ZMRequest; use ZM\Requests\ZMRequest;
class MessageUtil class MessageUtil
@ -55,6 +58,10 @@ class MessageUtil
return false; return false;
} }
public static function isAtMe($msg, $me_id) {
return strpos($msg, CQ::at($me_id)) !== false;
}
/** /**
* 通过本地地址返回图片的 CQ * 通过本地地址返回图片的 CQ
* type == 0 : 返回图片的 base64 CQ * type == 0 : 返回图片的 base64 CQ
@ -76,4 +83,80 @@ class MessageUtil
} }
return ""; 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;
}
} }

View File

@ -4,14 +4,110 @@
namespace ZM\Utils; 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 class ProcessManager
{ {
public static function runOnTask($param, $timeout = 0.5, $dst_worker_id = -1) { public static function workerAction($src_worker_id, $data) {
return server()->taskwait([ $server = server();
"action" => "runMethod", switch ($data["action"] ?? '') {
"class" => $param["class"], case "eval":
"method" => $param["method"], eval($data["data"]);
"params" => $param["params"] break;
], $timeout, $dst_worker_id); 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"]);
}
}
} }
} }

View File

@ -6,22 +6,38 @@ namespace ZM\Utils;
use Exception; use Exception;
use Psy\Shell; use Psy\Shell;
use Swoole\Event; use ZM\Annotation\Command\TerminalCommand;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Event\EventManager;
use ZM\Framework; use ZM\Framework;
class Terminal class Terminal
{ {
/** /**
* @param string $cmd * @param string $cmd
* @param $resource
* @return bool * @return bool
* @noinspection PhpMissingReturnTypeInspection * @noinspection PhpMissingReturnTypeInspection
* @noinspection PhpUnused * @noinspection PhpUnused
* @throws Exception
*/ */
public static function executeCommand(string $cmd, $resource) { public static function executeCommand(string $cmd) {
$it = explodeMsg($cmd); $it = explodeMsg($cmd);
switch ($it[0] ?? '') { 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': case 'logtest':
Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)"); Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)");
Console::error("This is error msg. (0)"); Console::error("This is error msg. (0)");
@ -35,7 +51,8 @@ class Terminal
$class_name = $it[1]; $class_name = $it[1];
$function_name = $it[2]; $function_name = $it[2];
$class = new $class_name([]); $class = new $class_name([]);
$class->$function_name(); $r = $class->$function_name();
if (is_string($r)) Console::success($r);
return true; return true;
case 'psysh': case 'psysh':
if (Framework::$argv["disable-coroutine"]) { if (Framework::$argv["disable-coroutine"]) {
@ -43,6 +60,11 @@ class Terminal
} else } else
Console::error("Only \"--disable-coroutine\" mode can use psysh!!!"); Console::error("Only \"--disable-coroutine\" mode can use psysh!!!");
return true; 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': case 'bc':
$code = base64_decode($it[1] ?? '', true); $code = base64_decode($it[1] ?? '', true);
try { try {
@ -57,7 +79,6 @@ class Terminal
Console::log($it[2], $it[1]); Console::log($it[2], $it[1]);
return true; return true;
case 'stop': case 'stop':
Event::del($resource);
ZMUtil::stop(); ZMUtil::stop();
return false; return false;
case 'reload': case 'reload':
@ -67,8 +88,35 @@ class Terminal
case '': case '':
return true; return true;
default: default:
Console::info("Command not found: " . $cmd); $dispatcher = new EventDispatcher(TerminalCommand::class);
return true; $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(), ">>> ");
} }
} }
} }

View File

@ -4,13 +4,10 @@
namespace ZM\Utils; namespace ZM\Utils;
use Co;
use Exception; use Exception;
use Swoole\Event; use Swoole\Process;
use Swoole\Timer;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Store\LightCache; use ZM\Framework;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock; use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic; use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf; use ZM\Store\ZMBuf;
@ -24,39 +21,21 @@ class ZMUtil
if (SpinLock::tryLock("_stop_signal") === false) return; if (SpinLock::tryLock("_stop_signal") === false) return;
Console::warning(Console::setColor("Stopping server...", "red")); Console::warning(Console::setColor("Stopping server...", "red"));
if (Console::getLevel() >= 4) Console::trace(); if (Console::getLevel() >= 4) Console::trace();
LightCache::savePersistence();
if (ZMBuf::$terminal !== null)
Event::del(ZMBuf::$terminal);
ZMAtomic::get("stop_signal")->set(1); ZMAtomic::get("stop_signal")->set(1);
try { for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
LightCache::set('stop', 'OK'); if (Process::kill(zm_atomic("_#worker_" . $i)->get(), 0))
} catch (Exception $e) { Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1);
} }
server()->shutdown(); server()->shutdown();
server()->stop(); server()->stop();
} }
/** /**
* @param int $delay
* @throws Exception * @throws Exception
*/ */
public static function reload($delay = 800) { public static function reload() {
if (server()->worker_id !== -1) { zm_atomic("_int_is_reload")->set(1);
Console::info(server()->worker_id); system("kill -INT " . intval(server()->master_pid));
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 getModInstance($class) { public static function getModInstance($class) {
@ -69,6 +48,22 @@ class ZMUtil
} }
public static function sendActionToWorker($target_id, $action, $data) { public static function sendActionToWorker($target_id, $action, $data) {
Console::verbose($action . ": " . $data);
server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id); 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
)
);
}
} }

View File

@ -3,6 +3,7 @@
use Swoole\Atomic; use Swoole\Atomic;
use Swoole\Coroutine; use Swoole\Coroutine;
use Swoole\WebSocket\Server; use Swoole\WebSocket\Server;
use Symfony\Component\VarDumper\VarDumper;
use ZM\API\ZMRobot; use ZM\API\ZMRobot;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM; 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 { function onebot_target_id_name($message_type): string {
return ($message_type == "group" ? "group_id" : "user_id"); return ($message_type == "group" ? "group_id" : "user_id");
} }
@ -255,13 +254,21 @@ function zm_sleep($s = 1): bool {
return true; 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) { function zm_timer_after($ms, callable $callable) {
Swoole\Timer::after($ms, function () use ($callable) { Swoole\Timer::after($ms, function () use ($callable) {
@ -287,10 +294,14 @@ function zm_timer_tick($ms, callable $callable) {
if (zm_cid() === -1) { if (zm_cid() === -1) {
return go(function () use ($ms, $callable) { return go(function () use ($ms, $callable) {
Console::debug("Adding extra timer tick of " . $ms . " ms"); 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 { } 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('.'); elseif (LOAD_MODE == 2) return realpath('.');
return null; 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); }