Compare commits

...

9 Commits
2.3.5 ... 2.4.2

Author SHA1 Message Date
jerry
d699a152d5 update to 2.4.2 version (build 402)
change WORKING_DIR constant
change logic of savePersistence()
add LightCache addPersistence() and removePersistence() method
add `./zhamao` command
2021-03-27 16:30:15 +08:00
jerry
beef44ea50 update docs: fix picture 2021-03-25 17:22:07 +08:00
jerry
b991a2da7b update docs: fix picture 2021-03-25 17:20:00 +08:00
jerry
bbe4addd83 update to 2.4.1 version (build 401)
fix startup warning bug
2021-03-25 17:11:35 +08:00
jerry
600829645d fix init command 2021-03-25 16:56:08 +08:00
jerry
68280cfe7e fix init command 2021-03-25 16:50:32 +08:00
jerry
c5523aa95d reset global config 2021-03-25 16:19:09 +08:00
jerry
93a68a5582 update to v2.4.0 (build 400)
add systemd:generate command
add check:config command
init command add `--force|-F` option
add MessageUtil function `addShortCommand()`
clear debug message
2021-03-25 16:18:09 +08:00
jerry
6155236d3c 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
2021-03-24 23:34:46 +08:00
46 changed files with 1398 additions and 356 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

@@ -1,31 +0,0 @@
#!/usr/bin/env php
<?php /** @since 1.2 */
switch ($argv[1] ?? '') {
case '--generate':
case '':
generate($argv);
break;
case '--help':
case '-h':
default:
echo "\nUsage: " . $argv[0] . " [OPTION]\n";
echo "\nzhamao-framework systemd generator.";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n --generate\tGenerate a systemd service file\n\n";
break;
}
function generate($argv) {
$s = "[Unit]\nDescription=zhamao-framework Daemon\nAfter=rc-local.service\n\n[Service]\nType=simple";
$s .= "\nUser=" . exec("whoami");
$s .= "\nGroup=" . exec("groups | awk '{print $1}'");
$s .= "\nWorkingDirectory=" . getcwd();
if ($argv[0] == "systemd" && !file_exists(getcwd() . '/systemd'))
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server";
else
$s .= "\nExecStart=" . getcwd() . "/bin/start server";
$s .= "\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n";
@mkdir(getcwd() . "/resources/");
file_put_contents(getcwd() . "/resources/zhamao.service", $s);
echo "File successfully generated. Path: " . getcwd() . "/resources/zhamao.service\n";
}

View File

@@ -29,10 +29,10 @@
"symfony/polyfill-mbstring": "^1.20",
"symfony/console": "^5.1",
"symfony/routing": "^5.1",
"zhamao/connection-manager": "*@dev",
"zhamao/console": "^1.0",
"zhamao/console": "^1.0.10",
"zhamao/config": "^1.0",
"zhamao/request": "*@dev"
"zhamao/request": "^1.1",
"zhamao/connection-manager": "^1.0"
},
"suggest": {
"ext-ctype": "*",

View File

@@ -1,7 +1,6 @@
<?php
/** @noinspection PhpFullyQualifiedNameUsageInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
global $config;
/** bind host */
$config['host'] = '0.0.0.0';
@@ -39,7 +38,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 +111,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;

View File

@@ -1,3 +1,23 @@
# 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先放一放。
```
## 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 进程处理密集运算
> 新开个坑有时间补上。__填坑标记__

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -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 连接号。
当 `$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);
```
![](../assets/img/CB86FE26-7EC7-4480-83FF-DE29285D1C7B.png)
## 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::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机器人连接框架后输出信息
```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,56 @@
# 更新日志v2 版本)
# v2.4.2 (build 402)
> 更新时间202.3.27
- 更改:`WORKING_DIR` 常量的含义
- 修复:未指定 `--remote-terminal` 参数时还依旧开启远程终端的 bug
- 删除:`phar_classloader()` 全局方法
- 更改:持久化存储 LightCache 的逻辑,修复一个愚蠢的容易造成误用的方式
- 新增LightCache 方法 `addPersistence()``removePersistence()`
- 新增:框架启动短指令 `./zhamao``php zhamao`
## v2.4.1 (build 401)
> 更新时间2021.3.25
- 修复:开启框架时导致的报错
## v2.4.0build 400
> 更新时间2021.3.25
- 新增:检查全局配置文件的命令
- 新增:全局配置文件更新记录
- 依赖变更:**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()` 函数
- 新增:`vendor/bin/start systemd:generate` 生成 systemd 配置文件的功能
- 新增:`vendor/bin/start check:config` 检查配置文件更新的命令
- 新增:`vendor/bin/start init` 新增 `--force` 参数,覆盖现有文件重新生成
- 新增MessageUtil 新增方法:`addShortCommand()`,用于快速添加静态文本问答回复的
以下是需要**手动更新**或**更换新写法**的部分:
- 配置文件 `global.php` 中的 `modules` 字段展开,内置模块的配置一律平铺到外面。详见 [更新日志 - 配置文件变更](/update/config)。
以下是默认机器人直接连接产生的变更:
- 2.4.0 新增了默认回复其他人 at 的消息,如果不需要,请将 `Hello.php` 中的 `changeAt()``turingAPI()` 方法删除。
## v2.3.5 (build 398)
> 更新时间2021.3.23
@@ -15,7 +66,6 @@
- 规范代码,修复一个小报错的 bug
## v2.3.0
> 更新时间2021.3.16

View File

@@ -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,10 +102,12 @@ 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
- 更新日志:
- 更新日志v2: update/v2.md
- 更新日志v1: update/v1.md
- 配置文件更新日志: update/config.md
- <u>炸毛框架 v1</u>: https://docs-v1.zhamao.xin/

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;
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

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,60 @@
<?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("检查配置文件是否和框架当前版本有更新");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
if (LOAD_MODE !== 1) {
$output->writeln("<error>仅限在Composer依赖模式中使用此命令</error>");
return Command::FAILURE;
}
$current_cfg = getcwd() . "/config/";
$remote_cfg = include_once FRAMEWORK_ROOT_DIR . "/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>");
} else {
$output->writeln("<info>配置文件暂无更新!</info>");
}
return Command::SUCCESS;
}
/**
* @noinspection PhpIncludeInspection
*/
private function check($remote, $local, OutputInterface $out) {
$local_file = include_once getcwd() . "/config/".$local;
foreach($remote as $k => $v) {
if (!isset($local_file[$k])) {
$out->writeln("<comment>配置文件 ".$local . " 需要更新!(当前配置文件缺少 `$k` 字段配置)</comment>");
$this->need_update = true;
}
}
}
}

View File

@@ -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("<info>成功停止!</info>");
return Command::SUCCESS;

View File

@@ -6,6 +6,7 @@ namespace ZM\Command;
use Phar;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class InitCommand extends Command
@@ -26,6 +27,9 @@ class InitCommand extends Command
protected function configure() {
$this->setDescription("Initialize framework starter | 初始化框架运行的基础文件");
$this->setDefinition([
new InputOption("force", "F", null, "强制重制,覆盖现有文件")
]);
$this->setHelp("此命令将会解压以下文件到项目的根目录:\n" . implode("\n", $this->getExtractFiles()));
// ...
}
@@ -33,9 +37,10 @@ class InitCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): int {
if (LOAD_MODE === 1) { // 从composer依赖而来的项目模式最基本的需要初始化的模式
$output->writeln("<comment>Initializing files</comment>");
$base_path = LOAD_MODE_COMPOSER_PATH;
$base_path = WORKING_DIR;
$args = $input->getOption("force");
foreach ($this->extract_files as $file) {
if (!file_exists($base_path . $file)) {
if (!file_exists($base_path . $file) || $args) {
$info = pathinfo($file);
@mkdir($base_path . $info["dirname"], 0777, true);
echo "Copying " . $file . PHP_EOL;
@@ -45,6 +50,12 @@ class InitCommand extends Command
echo "Skipping " . $file . " , file exists." . PHP_EOL;
}
}
echo "Copying ./zhamao\n";
file_put_contents(
$base_path."/zhamao",
"#!/usr/bin/env php\n<?php require_once \"vendor/autoload.php\";(new ZM\ConsoleApplication(\"zhamao-framework\"))->initEnv()->run();"
);
chmod($base_path."/zhamao", 0755);
$autoload = [
"psr-4" => [
"Module\\" => "src/Module",
@@ -67,8 +78,8 @@ class InitCommand extends Command
}
}
file_put_contents($base_path . "/composer.json", json_encode($composer, 64 | 128 | 256));
$output->writeln("<info>Executing composer update command</info>");
exec("composer update");
$output->writeln("<info>Executing composer command: `composer dump-autoload`</info>");
exec("composer dump-autoload");
echo PHP_EOL;
} else {
echo("Error occurred. Please check your updates.\n");

View File

@@ -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)"),

View File

@@ -13,8 +13,30 @@ class SystemdCommand extends Command
// the name of the command (the part after "bin/console")
protected static $defaultName = 'systemd:generate';
protected function configure() {
$this->setDescription("生成框架的 systemd 配置文件");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
//TODO: 写一个生成systemd配置的功能给2.0
$path = $this->generate();
$output->writeln("<info>成功生成 systemd 文件,位置:".$path."</info>");
$output->writeln("<info>有关如何使用 systemd 配置文件,请访问 `https://github.com/zhamao-robot/zhamao-framework/issues/36`</info>");
return Command::SUCCESS;
}
private function generate() {
$s = "[Unit]\nDescription=zhamao-framework Daemon\nAfter=rc-local.service\n\n[Service]\nType=simple";
$s .= "\nUser=" . exec("whoami");
$s .= "\nGroup=" . exec("groups | awk '{print $1}'");
$s .= "\nWorkingDirectory=" . getcwd();
if (LOAD_MODE == 1) {
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server";
} else {
$s .= "\nExecStart=" . getcwd() . "/bin/start server";
}
$s .= "\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n";
@mkdir(getcwd() . "/resources/");
file_put_contents(getcwd() . "/resources/zhamao.service", $s);
return getcwd() . "/resources/zhamao.service";
}
}

View File

@@ -5,6 +5,7 @@ namespace ZM;
use Exception;
use ZM\Command\CheckConfigCommand;
use ZM\Command\DaemonReloadCommand;
use ZM\Command\DaemonStatusCommand;
use ZM\Command\DaemonStopCommand;
@@ -14,12 +15,12 @@ use ZM\Command\RunServerCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
use ZM\Command\SystemdCommand;
class ConsoleApplication extends Application
{
const VERSION_ID = 398;
const VERSION = "2.3.5";
const VERSION_ID = 402;
const VERSION = "2.4.2";
public function __construct(string $name = 'UNKNOWN') {
define("ZM_VERSION_ID", self::VERSION_ID);
@@ -27,35 +28,24 @@ class ConsoleApplication extends Application
parent::__construct($name, ZM_VERSION);
}
public function initEnv(): ConsoleApplication {
public function initEnv($with_default_cmd = ""): ConsoleApplication {
$this->selfCheck();
if (!is_dir(__DIR__ . '/../../vendor')) {
define("LOAD_MODE", 1); // composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
} else {
define("LOAD_MODE", 0); // 源码模式
}
//if (LOAD_MODE === 0) $this->add(new BuildCommand()); //只有在git源码模式才能使用打包指令
if (LOAD_MODE === 0) define("WORKING_DIR", getcwd());
elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../"));
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
/** @noinspection PhpIncludeInspection */
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
}
define("WORKING_DIR", getcwd());
define("LOAD_MODE", is_dir(WORKING_DIR . "/src/ZM") ? 0 : 1);
define("FRAMEWORK_ROOT_DIR", realpath(__DIR__ . "/../../"));
if (LOAD_MODE == 0) {
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
$composer = json_decode(file_get_contents(WORKING_DIR . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";
$r = strtolower(trim(fgets(STDIN)));
if ($r === "" || $r === "y") {
$composer["autoload"]["psr-4"]["Module\\"] = "src/Module";
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
$r = file_put_contents(WORKING_DIR . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) {
echo "成功添加!请运行 composer dump-autoload \n";
exit(1);
exit(0);
} else {
echo "添加失败!请按任意键继续!";
fgets(STDIN);
@@ -73,8 +63,15 @@ class ConsoleApplication extends Application
new DaemonStopCommand(),
new RunServerCommand(), //运行主服务的指令控制器
new InitCommand(), //初始化用的用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令
new PureHttpCommand(), //纯HTTP服务器指令
new SystemdCommand()
]);
if (LOAD_MODE === 1) {
$this->add(new CheckConfigCommand());
}
if (!empty($with_default_cmd)) {
$this->setDefaultCommand($with_default_cmd);
}
/*
$command_register = ZMConfig::get("global", "command_register_class") ?? [];
foreach ($command_register as $v) {
@@ -100,7 +97,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;
}

View File

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

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

@@ -117,6 +117,17 @@ class EventDispatcher
public function dispatchEvent($v, $rule_func = null, ...$params) {
$q_c = $v->class;
$q_f = $v->method;
if ($q_c === "" && ($q_f instanceof \Closure)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数的事件触发过程!");
if ($rule_func !== null && !$rule_func($v)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 闭包函数下的 ruleFunc 判断为 false, 拒绝执行此方法。");
$this->status = self::STATUS_RULE_FAILED;
return false;
}
$this->store = $q_f(...$params);
$this->status = self::STATUS_NORMAL;
return true;
}
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在判断 " . $q_c . "::" . $q_f . " 方法下的 ruleFunc ...");
if ($rule_func !== null && !$rule_func($v)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 判断为 false, 拒绝执行此方法。");
@@ -152,6 +163,7 @@ class EventDispatcher
if ($before_result) {
try {
$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);
} catch (Exception $e) {
@@ -188,6 +200,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;

View File

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

View File

@@ -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,33 @@ 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 ($y) {
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 ($y) {
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 +403,11 @@ class Framework
Console::$theme = $y;
}
break;
case 'remote-terminal':
if ($y) {
$add_port = true;
}
break;
case 'show-php-ver':
default:
//Console::info("Calculating ".$x);
@@ -304,6 +416,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) {

View File

@@ -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,19 @@ 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();
$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);

View File

@@ -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;
@@ -16,8 +15,6 @@ class LightCache
{
/** @var Table|null */
private static $kv_table = null;
/** @var Table|null */
private static $kv_lock = null;
private static $config = [];
@@ -36,14 +33,13 @@ class LightCache
self::$kv_table->column("expire", Table::TYPE_INT);
self::$kv_table->column("data_type", Table::TYPE_STRING, 8);
$result = self::$kv_table->create();
self::$kv_lock = new Table($config["size"], $config["hash_conflict_proportion"]);
$result = $result && self::$kv_lock->create();
// 加载内容
if ($result === true && isset($config["persistence_path"])) {
if (file_exists($config["persistence_path"])) {
$r = json_decode(file_get_contents($config["persistence_path"]), true);
if ($r === null) $r = [];
foreach ($r as $k => $v) {
$write = self::set($k, $v, -2);
$write = self::set($k, $v);
Console::verbose("Writing LightCache: " . $k);
if ($write === false) {
self::$last_error = '可能是由于 Hash 冲突过多导致动态空间无法分配内存';
@@ -181,34 +177,50 @@ class LightCache
return $r;
}
public static function addPersistence($key) {
if (file_exists(self::$config["persistence_path"])) {
$r = json_decode(file_get_contents(self::$config["persistence_path"]), true);
if ($r === null) $r = [];
if (!isset($r[$key])) $r[$key] = null;
file_put_contents(self::$config["persistence_path"], json_encode($r, 64 | 128 | 256));
return true;
} else {
return false;
}
}
public static function removePersistence($key) {
if (file_exists(self::$config["persistence_path"])) {
$r = json_decode(file_get_contents(self::$config["persistence_path"]), true);
if ($r === null) $r = [];
if (isset($r[$key])) unset($r[$key]);
file_put_contents(self::$config["persistence_path"], json_encode($r, 64 | 128 | 256));
return true;
} else {
return false;
}
}
/**
* @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 = [];
foreach (self::$kv_table as $k => $v) {
if ($v["expire"] === -2) {
if (!empty(self::$config["persistence_path"])) {
$r = json_decode(file_get_contents(self::$config["persistence_path"]), true);
if ($r === null) $r = [];
foreach ($r as $k => $v) {
Console::verbose("Saving " . $k);
$r[$k] = self::parseGet($v);
$r[$k] = self::get($k);
}
file_put_contents(self::$config["persistence_path"], json_encode($r, 64 | 128 | 256));
}
if (self::$config["persistence_path"] == "") return;
if (file_exists(self::$config["persistence_path"])) {
$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) {

View File

@@ -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("内存不足,创建静态表失败!");

View File

@@ -7,6 +7,7 @@ namespace ZM\Store\Lock;
use Swoole\Coroutine;
use Swoole\Coroutine\System;
use Swoole\Table;
use ZM\Console\Console;
class SpinLock
{

View File

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

View File

@@ -16,10 +16,7 @@ class DataProvider
}
public static function getWorkingDir() {
if (LOAD_MODE == 0) return WORKING_DIR;
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
elseif (LOAD_MODE == 2) return realpath('.');
return null;
return WORKING_DIR;
}
public static function getFrameworkLink() {

View File

@@ -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,86 @@ 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;
}
public static function addShortCommand($command, string $reply) {
for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
ProcessManager::sendActionToWorker($i, "add_short_command", [$command, $reply]);
}
}
}

View File

@@ -4,14 +4,120 @@
namespace ZM\Utils;
use Co;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Console\Console;
use ZM\Event\EventDispatcher;
use ZM\Event\EventManager;
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 'add_short_command':
Console::verbose("Adding short command ".$data["data"][0]);
$obj = new CQCommand();
$obj->method = quick_reply_closure($data["data"][1]);
$obj->match = $data["data"][0];
$obj->class = "";
EventManager::addEvent(CQCommand::class, $obj);
break;
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"]);
}
}
}
}

View File

@@ -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(), ">>> ");
}
}
}

View File

@@ -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
)
);
}
}

View File

@@ -1,8 +1,9 @@
<?php /** @noinspection PhpUnused */ #plain
<?php #plain
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;
@@ -20,33 +21,13 @@ use Swoole\Coroutine\System;
use ZM\Context\ContextInterface;
function phar_classloader($p) {
$filepath = getClassPath($p);
if ($filepath === null) {
Console::debug("F:Warning: get class path wrongs.$p");
return;
}
try {
/** @noinspection PhpIncludeInspection */
require_once $filepath;
} catch (Exception $e) {
echo "Error when finding class: " . $p . PHP_EOL;
die;
}
}
function getClassPath($class_name) {
$dir = str_replace("\\", "/", $class_name);
$dir2 = WORKING_DIR . "/src/" . $dir . ".php";
//echo "@@@".$dir2.PHP_EOL;
$dir2 = str_replace("\\", "/", $dir2);
if (file_exists($dir2)) return $dir2;
else {
$dir = DataProvider::getWorkingDir() . "/src/" . $dir . ".php";
//echo "###".$dir.PHP_EOL;
if (file_exists($dir)) return $dir;
else return null;
}
else return null;
}
/**
@@ -243,8 +224,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 +234,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 +274,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);
});
}
}
@@ -349,8 +340,39 @@ function uuidgen($uppercase = false): string {
}
function working_dir() {
if (LOAD_MODE == 0) return WORKING_DIR;
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
elseif (LOAD_MODE == 2) return realpath('.');
return null;
return WORKING_DIR;
}
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); }
function quick_reply_closure($reply) {
return function() use ($reply){
return $reply;
};
}

4
zhamao Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
require_once "vendor/autoload.php";
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv("server")->run();