Compare commits

...

13 Commits
2.2.8 ... 2.3.0

Author SHA1 Message Date
jerry
456b102c15 update docs 2021-03-16 01:39:55 +08:00
jerry
cc57997abc update to build 387 2021-03-16 01:35:01 +08:00
jerry
19e61c7cc3 update to build 386
fix ZM_DATA equals null
add containsImage, getImageCQFromLocal function for MessageUtil
2021-03-16 01:34:17 +08:00
jerry
f908513dca update to build 385
add CQObject for CQ
add after-stop action(set terminal level 0)
update global.php modules, add http_proxy_server
add MessageUtil.php for message parsing
add RouteManager::addStaticFileRoute() for quick handling static file
finish onTask function finally!!
add TaskManager::runTask()
2021-03-15 02:54:16 +08:00
jerry
7dc39e6ada update to 2.2.11 verion
add build version (start from 384)
make 启动中 log as verbose
remove console input
fix pure http server bug
add error handler for zm_timer_tick and zm_timer_after
2021-03-13 15:16:10 +08:00
jerry
b0be53554d Merge remote-tracking branch 'origin/master' 2021-03-13 03:03:14 +08:00
jerry
b98048bd39 update docs 2021-03-13 03:03:01 +08:00
Whale
fffd3fdc95 Update README.md 2021-03-12 19:45:53 +08:00
jerry
dea9ed2ccd update docs 2021-03-08 00:56:35 +08:00
jerry
de5744c9e4 update to 2.2.10 version
add build-runtime.sh
remove debug msg when stopping
add --show-php-ver argument for server
2021-03-08 00:48:51 +08:00
jerry
a23f3d8f16 update to 2.2.9 version
update reply() to support quick operation
fix reload bug
fix reply() bug
2021-03-06 17:22:42 +08:00
jerry
0c24bfdedd fix a motd bug 2021-03-02 21:31:06 +08:00
jerry
c0b95c6840 delete workflows 2021-03-02 21:27:04 +08:00
37 changed files with 846 additions and 393 deletions

View File

@@ -1,18 +0,0 @@
name: ci
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: shimataro/ssh-key-action@v2
with:
python-version: 3.x
key: ${{ secrets.VPS_SSH }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- run: pip install mkdocs-material
- run: mkdocs build -d distribute && cd distribute/ && ssh root@139.199.77.130 rm -rf /var/www/docs_v2/\* && scp -r ./* root@139.199.77.130:/var/www/docs_v2/

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ composer.lock
/bin/.phpunit.result.cache
/resources/zhamao.service
.phpunit.result.cache
.daemon_pid
.daemon_pid
/runtime/

View File

@@ -66,6 +66,11 @@ public function index() {
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
## 下载源码
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和 clone 仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
例如:`git clone https://fgit.zhamao.me/zhamao-robot/zhamao-framework.git`
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。

View File

@@ -21,9 +21,9 @@ function generate($argv) {
$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 --disable-console-input";
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server";
else
$s .= "\nExecStart=" . getcwd() . "/bin/start server --disable-console-input";
$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);

226
build-runtime.sh Executable file
View File

@@ -0,0 +1,226 @@
#!/usr/bin/env bash
_php_ver="7.4.16"
_libiconv_ver="1.15"
_openssl_ver="1.1.1j"
_swoole_ver="4.6.3"
_home_dir=$(pwd)"/"
function checkEnv() {
echo -n "检测核心组件... "
_msg="请通过包管理安装此依赖!"
type git >/dev/null 2>&1 || { echo "失败git 不存在!"$_msg; return 1; }
type gcc >/dev/null 2>&1 || { echo "失败gcc 不存在!"$_msg; return 1; }
type g++ >/dev/null 2>&1 || { echo "失败g++ 不存在!"$_msg; return 1; }
type unzip >/dev/null 2>&1 || { echo "失败unzip 不存在!"$_msg; return 1; }
type autoconf >/dev/null 2>&1 || { echo "失败autoconf 不存在!"; return 1; }
type pkg-config >/dev/null 2>&1 || { echo "失败pkg-config 不存在!"$_msg; return 1; }
type wget >/dev/null 2>&1 || type curl >/dev/null 2>&1 || { echo "失败curl/wget 不存在!"$_msg; return 1; }
echo "完成!"
echo "如果下载过程中出现错误,请删除 runtime/ 文件夹重试!"
echo "此脚本安装的php/swoole均为最小版本不含其他扩展如zip、xml、gd"
echo -n "如果编译过程缺少依赖,请通过包管理安装对应的依赖![按回车继续] "
# shellcheck disable=SC2034
read ents
}
function downloadIt() {
downloader="wget"
type wget >/dev/null 2>&1 || { downloader="curl"; }
if [ "$downloader" = "wget" ]; then
_down_prefix="O"
else
_down_prefix="o"
fi
_down_symbol=0
if [ ! -f "$2" ]; then
$downloader "$1" -$_down_prefix "$2" >/dev/null 2>&1 && \
echo "完成!" && _down_symbol=1
else
echo "已存在!" && _down_symbol=1
fi
if [ $_down_symbol == 0 ]; then
echo "失败!请检查网络连接!"
rm -rf "$2"
return 1
fi
return 0
}
function downloadAll() {
# 创建文件夹
mkdir "$_home_dir""runtime" >/dev/null 2>&1
mkdir "$_home_dir""runtime/tmp_download" >/dev/null 2>&1
mkdir "$_home_dir""runtime/cellar" >/dev/null 2>&1
_down_dir=$_home_dir"runtime/tmp_download/"
# 下载PHP
echo -n "正在下载 php 源码... "
downloadIt "http://mirrors.sohu.com/php/php-$_php_ver.tar.gz" "$_down_dir""php.tar.gz" || { exit; }
# 下载libiconv
echo -n "正在下载 libiconv 源码... "
downloadIt "https://mirrors.tuna.tsinghua.edu.cn/gnu/libiconv/libiconv-$_libiconv_ver.tar.gz" "$_down_dir""libiconv.tar.gz" || { exit; }
echo -n "正在下载 openssl 源码... "
downloadIt "http://mirrors.cloud.tencent.com/openssl/source/openssl-$_openssl_ver.tar.gz" "$_down_dir""openssl.tar.gz" || { exit; }
echo -n "正在下载 swoole 源码... "
downloadIt "https://dl.zhamao.me/swoole/swoole-$_swoole_ver.tgz" "$_down_dir""swoole.tar.gz" || { exit; }
echo -n "正在下载 composer ... "
downloadIt "https://mirrors.aliyun.com/composer/composer.phar" "$_home_dir""runtime/cellar/composer" || { exit; }
#echo -n "正在下载 libcurl 源码... "
#downloadIt "https://curl.se/download/curl-7.75.0.tar.gz" "$_down_dir""libcurl.tar.gz" || { exit; }
}
function compileIt() {
_down_dir="$_home_dir""runtime/tmp_download/"
_source_dir="$_home_dir""runtime/tmp_source/"
_cellar_dir="$_home_dir""runtime/cellar/"
case $1 in
"libiconv")
if [ -f "$_cellar_dir""libiconv/bin/iconv" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libiconv.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libiconv-"$_libiconv_ver && \
./configure --prefix="$_cellar_dir""libiconv" >/dev/null 2>&1 && \
make -j4 >/dev/null 2>&1 && \
make install >/dev/null 2>&1 && \
echo "完成!"
;;
"libzip")
if [ -f "$_cellar_dir""libzip/bin/libzip" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libzip.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libzip-1.7.3" && \
./configure --prefix="$_cellar_dir""libzip" && \
make -j4 && \
make install && \
echo "完成!"
;;
"libcurl")
if [ -f "$_cellar_dir""libcurl/bin/libcurl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libcurl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libcurl-7.75.0" && \
./configure --prefix="$_cellar_dir""libcurl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"php")
if [ -f "$_cellar_dir""php/bin/php" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""php.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""php-"$_php_ver && \
./buildconf --force && \
PKG_CONFIG_PATH="$_cellar_dir""openssl/lib/pkgconfig" ./configure --prefix="$_cellar_dir""php" \
--with-config-file-path="$_home_dir""runtime/etc" \
--disable-fpm \
--enable-cli \
--enable-posix \
--enable-ctype \
--enable-mysqlnd \
--enable-pdo \
--enable-pcntl \
--with-openssl="$_cellar_dir""openssl" \
--enable-sockets \
--disable-xml \
--disable-xmlreader \
--disable-xmlwriter \
--without-libxml \
--disable-dom \
--without-sqlite3 \
--without-pdo-sqlite \
--disable-simplexml \
--with-pdo-mysql=mysqlnd \
--with-zlib \
--with-iconv="$_cellar_dir""libiconv" \
--enable-phar && \
make -j4 && \
make install && \
cp "$_source_dir""php-$_php_ver/php.ini-production" "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
"openssl")
if [ -f "$_cellar_dir""openssl/bin/openssl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""openssl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""openssl-""$_openssl_ver" && \
./config --prefix="$_cellar_dir""openssl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"swoole")
"$_home_dir"runtime/cellar/php/bin/php --ri swoole >/dev/null 2>&1
# shellcheck disable=SC2181
if [ $? == 0 ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""swoole.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""swoole-""$_swoole_ver" && \
PATH="$_cellar_dir""php/bin:$PATH" phpize && \
PATH="$_cellar_dir""php/bin:$PATH" ./configure --prefix="$_cellar_dir""php" \
--enable-sockets \
--enable-http2 \
--enable-openssl \
--with-openssl-dir="$_cellar_dir""openssl" \
--enable-mysqlnd && \
make -j4 && \
make install && \
echo "extension=swoole.so" >> "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
esac
}
function compileAll() {
_down_dir=$_home_dir"runtime/tmp_download/"
_source_dir=$_home_dir"runtime/tmp_source/"
mkdir "$_source_dir" >/dev/null 2>&1
mkdir "$_home_dir""runtime/etc" >/dev/null 2>&1
echo -n "正在编译 libiconv ... "
compileIt libiconv || { return 1; }
#echo -n "正在编译 libcurl ... "
#compileIt libcurl || { exit; }
echo -n "正在编译 openssl ... "
compileIt openssl || { return 1; }
#echo -n "正在编译 libzip ... "
#compileIt libzip || { exit; }
echo -n "正在编译 php ... "
compileIt php || { return 1; }
echo -n "正在编译 swoole ... "
compileIt swoole || { return 1; }
return 0
}
function linkBin(){
mkdir "$_home_dir""runtime/bin" >/dev/null 2>&1
ln -s "$_home_dir""runtime/cellar/php/bin/php" "$_home_dir""runtime/bin/php" >/dev/null 2>&1
echo "runtime/cellar/php/bin/php runtime/cellar/composer \$@" > "$_home_dir""runtime/bin/composer" && chmod +x "$_home_dir""runtime/bin/composer"
echo "Done!"
runtime/bin/composer config repo.packagist composer https://mirrors.aliyun.com/composer/
}
checkEnv && \
downloadAll && \
compileAll && \
linkBin && \
echo "成功部署所有环境!" && \
echo -e "composer更新依赖\t\"runtime/bin/composer update\"" && \
echo -e "启动框架(源码模式)\t\"runtime/bin/php bin/start server\"" && \
echo -e "启动框架(普通模式)\t\"runtime/bin/php vendor/bin/start server\""

View File

@@ -1,9 +1,8 @@
{
"name": "zhamao/framework",
"description": "High performance QQ robot and web server development framework",
"description": "High performance chat robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "2.2.8",
"extra": {
"exclude_annotate": [
"src/ZM"
@@ -11,12 +10,8 @@
},
"authors": [
{
"name": "whale",
"email": "crazysnowcc@gmail.com"
},
{
"name": "swift",
"email": "hugo_swift@yahoo.com"
"name": "jerry",
"email": "admin@zhamao.me"
}
],
"prefer-stable": true,
@@ -52,7 +47,6 @@
]
},
"require-dev": {
"swoole/ide-helper": "@dev",
"phpunit/phpunit": "^9.5"
"swoole/ide-helper": "@dev"
}
}

View File

@@ -27,7 +27,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
//'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'max_coroutine' => 300000,
//'task_worker_num' => 4,
@@ -109,15 +109,25 @@ $config['static_file_server'] = [
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\ZM\Event\ServerEventHandler::class,
// 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类
];
/** 服务器启用的外部第三方和内部插件 */
$config['modules'] = [
'onebot' => [
'onebot' => [ // 机器人解析模块,关闭后无法使用如@CQCommand等注解
'status' => true,
'single_bot_mode' => false
], // QQ机器人事件解析器如果取消此项则默认为 true 开启状态,否则你手动填写 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
]
],
];
return $config;

View File

@@ -85,7 +85,6 @@ bin/start server # 通过源码模式启动框架
- `--debug-mode`:启用调试模式,调试模式的作用是关闭一键协程化和终端交互,减少 Swoole 本身对代码逻辑的干扰(比如执行 `shell_exec()` 报错的话可以开启这个进行调试)。
- `--log-{mode}`:设置 log 等级。支持 `--log-debug``--log-verbose``--log-info``--log-warning``--log-error`
- `--log-theme`:设置终端信息的主题。这个选项适用于多种终端信息显示的兼容,例如白色终端和不支持颜色的终端。详见 [Console - 主题设置](/component/console/#_2)。
- `--disable-console-input`:关闭终端交互,如果你使用的不是 tmux、screen 而是直接将进程使用 systemd 等方式运行到 init 守护进程下,则需要关闭终端交互输入,关闭后不可以使用 `stop, reload, logtest` 等交互命令。
- `--disable-coroutine`:关闭一键协程化。
- `--daemon`:以守护进程方式运行框架,此参数将直接在输出 motd 后将进程挂到 init 下运行,后台常驻。
- `--watch`:监控 `src/` 目录下的文件变化,有变化则自动重新载入代码。开启监控需要安装 PHP 扩展inotify。使用 pecl 就可以安装:`pecl install inotify`

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,59 @@
# Token 验证
为了保障安全,框架支持给接入的 WebSocket 连接验证 Token如果不设置 Token 同时又将框架的端口暴露在公网将会非常危险。
炸毛框架兼容 OneBot 标准的机器人客户端,所以自带一个 Token 验证器。
关于 Access Token 方面的标准规范,请参考下面内容:
- [OneBot - 鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md)
- [go-cqhttp - 配置](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)
> 以 go-cqhttp 举例,如果要设置验证,则将 go-cqhttp 配置文件中的 `access_token` 项填入内容即可。
## 验证位置
框架对 Token 的验证是内置的,在事件 `open`WebSocket 连接接入时)触发。
如果是兼容 OneBot 标准的客户端接入,则一切都是兼容的。
如果是自定义的其他 WebSocket 客户端也想接入框架,那么其他 WebSocket 客户端也需要进行相应的设置才能利用此 Token 验证。
如果验证成功Token 符合要求)则分发事件 `@OnOpenEvent`,否则此事件不触发,同时断开 WebSocket 连接。
## 标准验证(字符串形式)
默认的情况下,在框架的全局配置文件 `global.php` 中,对配置项 `access_token` 填入与 OneBot 客户端相同的 `access_token` 即可实现鉴权。下面是一个最基本的和 go-cqhttp 设置鉴权配置:
go-cqhttp 的配置段:
```hjson
// 访问密钥, 强烈推荐在公网的服务器设置
access_token: "emhhbWFvLXJvYm90"
```
框架的配置文件配置段:
```php
/** onebot连接约定的token */
$config["access_token"] = 'emhhbWFvLXJvYm90';
```
然后重启框架和 go-cqhttp 即可。(其他 OneBot 客户端同理)
## 自定义验证Token 验证)
有些情况下,使用一个单一的字符串可能无法满足你对 Token 验证的安全需求,需要自定义一些判断模式才能满足,所以框架的 `access_token` 配置项支持动态的闭包函数自行编写判断逻辑,例如下面的一个例子,我可以让框架同时允许接入多个不同 token 的 WebSocket 连接:
```php
/** onebot连接约定的token */
$config["access_token"] = function($token){
$allow = ['emhhbWFvLXJvYm90','aXMtdmVyeS1nb29k'];
if (in_array($token, $allow)) return true;
else return false;
};
```
## 自定义验证open 事件)
当然,这里设置了自定义方式,其实你也可以在下一层的 `@OnOpenEvent` 注解事件中进行自定义内容和判断,具体见 `@OnOpenEvent` 的相关章节。

View File

@@ -51,7 +51,7 @@ public function hello() {
* @CQCommand("测试fd")
*/
public function testfd() {
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()"机器人QQ是".ctx()->getRobotId());
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()."机器人QQ是".ctx()->getRobotId());
}
```
@@ -421,4 +421,5 @@ public function argTest1() {
<chat-box>
) test abc 334 argtest
( 参数内容abc, 334, argtest
</chat-box>
</chat-box>

View File

@@ -128,8 +128,9 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
解析 CQ 码。
- 参数`getCQ($msg);`:要解析出 CQ 码的消息。
- 返回:`数组 | null`,见下表
- 定义`getCQ($msg, $is_object = false)`
- 参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject` 对象此对象的属性和下表相同。2.3.0+ 版本可用)
- 返回:`数组 | CQObject | null`,见下表。
| 键名 | 说明 |
| ------ | ------------------------------------------------------------ |
@@ -140,6 +141,10 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
### CQ::getAllCQ()
定义:`CQ::getAllCQ($msg, $is_object = false)`
参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject[]` 对象数组此对象的属性和上面的表格内相同。2.3.0+ 版本可用)
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
```php
@@ -174,7 +179,7 @@ CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](/assets/face_id.html)。
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](https://static.zhamao.me/face_id.html)。
```php
/**

View File

@@ -34,4 +34,21 @@ DataProvider 是框架内提供的一个简易的文件管理类。
定义:`loadFromJson($filename)`
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
## 其他文件读取
框架比较贴近原生的 PHP所以推荐直接使用原生的方法来读写文件`file_get_contents``file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。
```php
// 读取框架工作目录的文件 composer.json 文件
$r = file_get_contents(working_dir() . "/composer.json");
// 写入 Linux 临时目录下的文件
file_put_contents("/tmp/test.txt", "hello world");
```
!!! warning "注意"
在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。

View File

@@ -221,6 +221,27 @@
| tick_ms | `int`**必填**,间隔的毫秒数,例如 1 秒间隔为 `1000`,范围大于 0小于 86400000。 | | |
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0范围是 0{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
## OnTask()
定义一个在工作进程中运行的任务函数。详情见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------- |
| 名称 | `@OnTask` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnTask` |
| 适用位置 | 方法 |
| 返回值处理 | 有,返回 Worker 进程的结果 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------ | ---- |
| task_name | `string`**必填**,任务函数的名称,不建议重复。 | | |
| rule | 设置触发前提PHP 代码,返回 bool 值即可,参考 OnRequestEvent | 限定是否执行 | 空 |
## OnSetup()
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。

View File

@@ -10,38 +10,40 @@
框架的全局配置文件在 `config/global.php` 文件中。下面是配置文件的各个选项,请根据自己的需要自行配置。
| 配置名称 | 说明 | 默认值 |
| :--------------------------- | ------------------------------------------------ | ---------------------------- |
| `host` | 框架监听的地址 | 0.0.0.0 |
| `port` | 框架监听的端口 | 20001 |
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
| `debug_mode` | 框架是否启动 debug 模式 | false |
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无 | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
| `info_level` | 终端日志显示等级0-4 | 2 |
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
| 配置名称 | 说明 | 默认值 |
| :--------------------------- | ------------------------------------------------------------ | ---------------------------- |
| `host` | 框架监听的地址 | 0.0.0.0 |
| `port` | 框架监听的端口 | 20001 |
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
| `debug_mode` | 框架是否启动 debug 模式 | false |
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无,相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
| `info_level` | 终端日志显示等级0-4 | 2 |
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
### 子表 **swoole**
| 配置名称 | 说明 | 默认值 |
| --------------- | ------------------------------------------------------------ | ----------------------------------- |
| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` |
| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 |
| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 |
| `max_coroutine` | 最大协程并发数 | 300000 |
| 配置名称 | 说明 | 默认值 |
| ----------------------- | ------------------------------------------------------------ | ----------------------------------- |
| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` |
| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 |
| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 |
| `max_coroutine` | 最大协程并发数 | 300000 |
| `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) |
| `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` |
### 子表 **light_cache**

View File

@@ -1,5 +1,45 @@
# 更新日志v2 版本)
## 2.3.0
> 更新时间2021.3.16
- 新增MessageUtil 消息处理工具类
- 新增TaskManager封装了 TaskWorker 进程的应用
- 新增CQObject使用 `CQ::getCQ()` 可获取对象形式的 CQ 码解析结果
- 新增:`@OnTask` 注解,绑定任务函数
- 新增RouteManager 路由管理类,可快速添加路由
- 修复:`ZM_DATA``DataProvider::getDataFolder()` 返回 false 的问题
- 优化:关闭显示停止框架后多余的输出信息
注:本次升级建议升级后合并全局配置文件,有一些新加的内容。
## 2.2.11
> 更新时间2021.3.13
- 新增:内部 ID 版本号ZM_VERSION_ID
- 优化:启动时 log 的等级
- 移除:终端输入命令
- 修复:纯 HTTP 服务器的启动 bug
- 新增:`zm_timer` 的报错处理,防止服务器直接崩掉
## v2.2.10
> 更新时间2021.3.8
- 新增:用户态 php 编译脚本 `build-runtime.sh`
- 移除:无用的调试信息
- 新增:`--show-php-ver` 启动参数
## v2.2.9
> 更新时间2021.3.6
- 更新:`reply()` 方法传入数组则变为快速相应的 API 操作
- 修复:在 Worker 进程下调用 `ZMUtil::reload()` 会导致一些奇怪的 bug
- 修复:`reply()` 时会 at 私聊成员的 bug由 go-cqhttp 导致)
## v2.2.8
> 更新时间2021.3.2

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,12 +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
- TaskWorker 管理: component/task-worker.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md
@@ -96,6 +102,7 @@ nav:
- 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md
- TaskWorker 提高并发: advanced/task-worker.md
- 开发实战教程:
- 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md

View File

@@ -6,6 +6,7 @@ use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
@@ -21,6 +22,14 @@ use ZM\Utils\ZMUtil;
*/
class Hello
{
/*
* 默认的图片监听路由对应目录,如需要使用可取消下面的注释,把上面的 /* 换成 /**
* @OnStart(-1)
*/
//public function onStart() {
// \ZM\Http\RouteManager::addStaticFileRoute("/images/", \ZM\Utils\DataProvider::getWorkingDir()."/zm_data/images/");
//}
/**
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
* @CQCommand(".reload",user_id=627577391)

View File

@@ -5,6 +5,7 @@ namespace ZM\API;
use ZM\Console\Console;
use ZM\Entity\CQObject;
class CQ
{
@@ -305,9 +306,10 @@ class CQ
/**
* 获取消息中第一个CQ码
* @param $msg
* @return array|null
* @param bool $is_object
* @return array|CQObject|null
*/
public static function getCQ($msg) {
public static function getCQ($msg, $is_object = false) {
if (($head = mb_strpos($msg, "[CQ:")) !== false) {
$key_offset = mb_substr($msg, $head);
$close = mb_strpos($key_offset, "]");
@@ -322,7 +324,7 @@ class CQ
}
$cq["start"] = $head;
$cq["end"] = $close + $head;
return $cq;
return !$is_object ? $cq : CQObject::fromArray($cq);
} else {
return null;
}
@@ -331,9 +333,10 @@ class CQ
/**
* 获取消息中所有的CQ码
* @param $msg
* @return array
* @param bool $is_object
* @return array|CQObject[]
*/
public static function getAllCQ($msg) {
public static function getAllCQ($msg, $is_object = false) {
$cqs = [];
$offset = 0;
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
@@ -352,7 +355,7 @@ class CQ
$cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1;
$cqs[] = $cq;
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
}
return $cqs;
}

View File

@@ -0,0 +1,36 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Rule;
/**
* Class OnTask
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnTask extends AnnotationBase implements Rule
{
/**
* @var string
* @Required()
*/
public $task_name;
/**
* @var string
*/
public $rule = "";
/**
* @return mixed
*/
public function getRule() {
return $this->rule;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class OnTaskEvent
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnTaskEvent extends OnSwooleEventBase
{
}

View File

@@ -17,7 +17,9 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Framework;
use ZM\Store\ZMAtomic;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
class PureHttpCommand extends Command
@@ -40,17 +42,26 @@ class PureHttpCommand extends Command
$output->writeln("<error>Directory error(" . ($input->getArgument('dir') ?? '.') . "): no such file or directory.</error>");
return self::FAILURE;
}
ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config');
$global = ZMConfig::get("global");
$host = $input->getOption("host") ?? $global["host"];
$port = $input->getOption("port") ?? $global["port"];
$index = ["index.html", "index.htm"];
$out = [
"listen" => $host.":".$port,
"version" => ZM_VERSION,
"web_root" => realpath($input->getArgument('dir') ?? '.'),
"index" => implode(",", $index)
];
Framework::printProps($out, $tty_width);
$server = new Server($host, $port);
$server->set(ZMConfig::get("global", "swoole"));
Console::init(0, $server);
Console::init(2, $server);
ZMAtomic::$atomics["request"] = [];
for ($i = 0; $i < 32; ++$i) {
ZMAtomic::$atomics["request"][$i] = new Atomic(0);
}
$index = ["index.html", "index.htm"];
$server->on("request", function (Request $request, Response $response) use ($input, $index, $server) {
ZMAtomic::$atomics["request"][$server->worker_id]->add(1);
HttpUtil::handleStaticPage(
@@ -60,10 +71,11 @@ class PureHttpCommand extends Command
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => $index
]);
echo "\r" . Coroutine::stats()["coroutine_peak_num"];
//echo "\r" . Coroutine::stats()["coroutine_peak_num"];
});
$server->on("start", function ($server) {
Process::signal(SIGINT, function () use ($server) {
echo "\r";
Console::warning("Server interrupted by keyboard.");
for ($i = 0; $i < 32; ++$i) {
$num = ZMAtomic::$atomics["request"][$i]->get();
@@ -75,13 +87,6 @@ class PureHttpCommand extends Command
});
Console::success("Server started. Use Ctrl+C to stop.");
});
$out = [
"host" => $host,
"port" => $port,
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => implode(", ", $index)
];
Console::printProps($out, $tty_width);
$server->start();
// return this if there was no problem running the command
// (it's equivalent to returning int(0))

View File

@@ -22,10 +22,11 @@ class RunServerCommand extends Command
new InputOption("log-warning", null, null, "调整消息等级到warning (log-level=1)"),
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("disable-console-input", null, null, "禁止终端输入内容 (废弃)"),
new InputOption("disable-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"),
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)"),
]);
$this->setDescription("Run zhamao-framework | 启动框架");

View File

@@ -18,9 +18,13 @@ use ZM\Utils\DataProvider;
class ConsoleApplication extends Application
{
const VERSION_ID = 387;
const VERSION = "2.3.0";
public function __construct(string $name = 'UNKNOWN') {
$version = json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "UNKNOWN";
parent::__construct($name, $version);
define("ZM_VERSION_ID", self::VERSION_ID);
define("ZM_VERSION", self::VERSION);
parent::__construct($name, ZM_VERSION);
}
public function initEnv() {
@@ -50,7 +54,7 @@ class ConsoleApplication extends Application
$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));
if ($r !== false) {
echo "成功添加!请重新进行 composer update \n";
echo "成功添加!请行 composer dump-autoload \n";
exit(1);
} else {
echo "添加失败!请按任意键继续!";

View File

@@ -104,25 +104,31 @@ class Context implements ContextInterface
* @param $msg
* @param bool $yield
* @return mixed
* @noinspection PhpMissingBreakStatementInspection
*/
public function reply($msg, $yield = false) {
switch ($this->getData()["message_type"]) {
case "group":
$operation["at_sender"] = false;
// no break
case "private":
case "discuss":
$this->setCache("has_reply", true);
$data = $this->getData();
$conn = $this->getConnection();
$operation["reply"] = $msg;
return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [
"context" => $data,
"operation" => $operation
]);
$data = $this->getData();
$conn = $this->getConnection();
if (!is_array($msg)) {
switch ($this->getData()["message_type"]) {
case "group":
case "private":
case "discuss":
$this->setCache("has_reply", true);
$operation["reply"] = $msg;
$operation["at_sender"] = false;
return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [
"context" => $data,
"operation" => $operation
]);
}
return false;
} else {
$operation = $msg;
return (new ZMRobot($conn))->setCallback(false)->callExtendedAPI(".handle_quick_operation", [
"context" => $data,
"operation" => $operation
]);
}
return false;
}
public function finalReply($msg, $yield = false) {

View File

@@ -0,0 +1,26 @@
<?php
namespace ZM\Entity;
class CQObject
{
public $type;
public $params;
public $start;
public $end;
public function __construct($type = "", $params = [], $start = 0, $end = 0) {
if ($type !== "") {
$this->type = $type;
$this->params = $params;
$this->start = $start;
$this->end = $end;
}
}
public static function fromArray($arr) {
return new CQObject($arr["type"], $arr["params"] ?? [], $arr["start"], $arr["end"]);
}
}

View File

@@ -100,6 +100,7 @@ class EventDispatcher
}
}
if ($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
//TODO:没有过滤before的false可能会导致一些问题先观望一下
} catch (InterruptException $e) {
$this->store = $e->return_var;
$this->status = self::STATUS_INTERRUPTED;

View File

@@ -17,6 +17,7 @@ use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Event;
use Swoole\Process;
use Throwable;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
@@ -26,6 +27,8 @@ use ZM\Annotation\Swoole\OnPipeMessageEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Annotation\Swoole\OnTask;
use ZM\Annotation\Swoole\OnTaskEvent;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
@@ -47,10 +50,8 @@ use ZM\Store\LightCacheInside;
use ZM\Store\MySQL\SqlPoolStorage;
use ZM\Store\Redis\ZMRedisPool;
use ZM\Store\WorkerCache;
use ZM\Store\ZMBuf;
use ZM\Utils\DataProvider;
use ZM\Utils\HttpUtil;
use ZM\Utils\Terminal;
use ZM\Utils\ZMUtil;
class ServerEventHandler
@@ -59,9 +60,9 @@ class ServerEventHandler
* @SwooleHandler("start")
*/
public function onStart() {
global $terminal_id;
//global $terminal_id;
$r = null;
if ($terminal_id !== null) {
/*if ($terminal_id !== null) {
ZMBuf::$terminal = $r = STDIN;
Event::add($r, function () use ($r) {
$fget = fgets($r);
@@ -78,13 +79,18 @@ class ServerEventHandler
Console::error("Uncaught error " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")");
}
});
}
}*/
Process::signal(SIGINT, function () use ($r) {
echo "\r";
Console::warning("Server interrupted(SIGINT) on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0);
ZMUtil::reload();
} else {
echo "\r";
Console::warning("Server interrupted(SIGINT) on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
}
});
if (Framework::$argv["daemon"]) {
$daemon_data = json_encode([
@@ -154,7 +160,7 @@ class ServerEventHandler
else server()->shutdown();
});
Console::info("Worker #{$server->worker_id} 启动中");
Console::verbose("Worker #{$server->worker_id} 启动中");
Framework::$server = $server;
//ZMBuf::resetCache(); //清空变量缓存
//ZMBuf::set("wait_start", []); //添加队列在workerStart运行完成前先让其他协程等待执行
@@ -228,7 +234,7 @@ class ServerEventHandler
$this->loadAnnotations(); //加载composer资源、phar外置包、注解解析注册等
//echo json_encode(debug_backtrace(), 128|256);
Console::success("Worker #" . $worker_id . " 已启动");
EventManager::registerTimerTick(); //启动计时器
//ZMBuf::unsetCache("wait_start");
set_coroutine_params(["server" => $server, "worker_id" => $worker_id]);
@@ -239,6 +245,7 @@ class ServerEventHandler
$dispatcher->dispatchEvents($server, $worker_id);
if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕");
else Console::warning("@OnStart 执行异常!");
Console::success("Worker #" . $worker_id . " 已启动");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -255,7 +262,7 @@ class ServerEventHandler
try {
Framework::$server = $server;
$this->loadAnnotations();
Console::debug("TaskWorker #" . $server->worker_id . " 已启动");
Console::success("TaskWorker #" . $server->worker_id . " 已启动");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@@ -311,7 +318,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
}
@@ -461,7 +468,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"open\": " . $error_msg);
Console::trace();
}
//EventHandler::callSwooleEvent("open", $server, $request);
@@ -508,7 +515,7 @@ class ServerEventHandler
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught Error " . get_class($e) . " when calling \"close\": " . $error_msg);
Console::error("Uncaught " . get_class($e) . " when calling \"close\": " . $error_msg);
Console::trace();
}
ManagerGM::popConnect($fd);
@@ -590,20 +597,54 @@ class ServerEventHandler
* @SwooleHandler("task")
* @param Server|null $server
* @param Server\Task $task
* @return mixed
* @noinspection PhpUnusedParameterInspection
* @return null
*/
public function onTask(?Server $server, Server\Task $task) {
$data = $task->data;
switch ($data["action"]) {
case "runMethod":
$c = $data["class"];
$ss = new $c();
$method = $data["method"];
$ps = $data["params"];
$task->finish($ss->$method(...$ps));
if (isset($task->data["task"])) {
$dispatcher = new EventDispatcher(OnTask::class);
$dispatcher->setRuleFunction(function ($v) use ($task) {
/** @var OnTask $v */
return $v->task_name == $task->data["task"];
});
$dispatcher->setReturnFunction(function ($return) {
EventDispatcher::interrupt($return);
});
$params = $task->data["params"];
try {
$dispatcher->dispatchEvents(...$params);
} catch (Throwable $e) {
$finish["throw"] = $e;
}
if ($dispatcher->status === EventDispatcher::STATUS_EXCEPTION) {
$finish["result"] = null;
$finish["retcode"] = -1;
} else {
$finish["result"] = $dispatcher->store;
$finish["retcode"] = 0;
}
if (zm_atomic("server_is_stopped")->get() === 1) {
return;
}
$task->finish($finish);
} else {
try {
$dispatcher = new EventDispatcher(OnTaskEvent::class);
$dispatcher->setRuleFunction(function ($v) {
/** @var OnTaskEvent $v */
return eval("return " . $v->getRule() . ";");
});
$dispatcher->dispatchEvents();
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught exception " . get_class($e) . " when calling \"task\": " . $error_msg);
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught " . get_class($e) . " when calling \"task\": " . $error_msg);
Console::trace();
}
}
return null;
}
/**
@@ -636,7 +677,8 @@ class ServerEventHandler
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
foreach ($dir as $v) {
if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) {
Console::verbose("Add " . $v . " to register path");
if (\server()->worker_id == 0)
Console::verbose("Add " . $v . " to register path");
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v);
}
}

View File

@@ -45,14 +45,15 @@ class Framework
self::$argv = $args;
//定义常量
include_once "global_defines.php";
ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config');
ZMConfig::setEnv($args["env"] ?? "");
if (ZMConfig::get("global") === false) {
die ("Global config load failed: " . ZMConfig::$last_error . "\nPlease init first!\n");
}
//定义常量
include_once "global_defines.php";
ZMAtomic::init();
try {
$sw = ZMConfig::get("global");
@@ -74,8 +75,6 @@ class Framework
die($e->getMessage());
}
try {
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
$this->server_set = ZMConfig::get("global", "swoole");
Console::init(
ZMConfig::get("global", "info_level") ?? 2,
self::$server,
@@ -86,25 +85,23 @@ class Framework
$timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai";
date_default_timezone_set($timezone);
$this->server_set = ZMConfig::get("global", "swoole");
$this->parseCliArgs(self::$argv);
self::$server->set($this->server_set);
// 打印初始信息
$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"];
$out["env"] = $args["env"] === null ? "default" : $args["env"];
$out["environment"] = $args["env"] === null ? "default" : $args["env"];
$out["log_level"] = Console::getLevel();
$out["version"] = ZM_VERSION;
$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 (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
$conf = ZMConfig::get("global", "sql_config");
$out["mysql_pool"] = $conf["database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"];
$out["mysql_pool"] = $conf["sql_database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"];
}
if (ZMConfig::get("global", "redis_config")["host"] !== "") {
$conf = ZMConfig::get("global", "redis_config");
@@ -113,10 +110,18 @@ class Framework
if (ZMConfig::get("global", "static_file_server")["status"] !== false) {
$out["static_file_server"] = "enabled";
}
if (self::$argv["show-php-ver"] !== false) {
$out["php_version"] = PHP_VERSION;
$out["swoole_version"] = SWOOLE_VERSION;
}
$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"));
self::$server->set($this->server_set);
Console::setServer(self::$server);
self::printMotd($tty_width);
global $asd;
@@ -170,6 +175,7 @@ class Framework
} catch (Exception $e) {
Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage());
Console::debug($e);
die;
}
}
@@ -190,6 +196,8 @@ class Framework
public function start() {
self::$server->start();
zm_atomic("server_is_stopped")->set(1);
Console::setLevel(0);
}
/**
@@ -288,6 +296,7 @@ class Framework
Console::$theme = $y;
}
break;
case 'show-php-ver':
default:
//Console::info("Calculating ".$x);
//dump($y);
@@ -350,6 +359,7 @@ class Framework
$line_width = [];
$line_data = [];
foreach ($out as $k => $v) {
start:
if (!isset($line_width[$current_line])) {
$line_width[$current_line] = $max_border - 2;
}
@@ -363,11 +373,7 @@ class Framework
if (strlen($tmp_line) > $line_width[$current_line]) { // 地方不够,另起一行
$line_data[$current_line] = str_replace("| ", "", $line_data[$current_line]);
++$current_line;
$line_data[$current_line] = " " . $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
$line_data[$current_line] .= $v;
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
++$current_line;
goto start;
} else { // 地方够,直接写到后面并另起一行
$line_data[$current_line] .= $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);

View File

@@ -9,6 +9,7 @@ use Symfony\Component\Routing\RouteCollection;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
class RouteManager
{
@@ -34,4 +35,26 @@ class RouteManager
self::$routes->add(md5($route_name), $route);
}
public static function addStaticFileRoute($route, $path) {
$tail = trim($route, "/");
$route_name = ($tail === "" ? "" : "/") . $tail . "/{filename}";
Console::debug("添加静态文件路由:" . $route_name);
$route = new Route($route_name, ['_class' => RouteManager::class, '_method' => "onStaticRoute"]);
//echo $path.PHP_EOL;
LightCacheInside::set("static_route", $route->getPath(), $path);
self::$routes->add(md5($route_name), $route);
}
public function onStaticRoute($params) {
$route_path = self::$routes->get($params["_route"])->getPath();
if(($path = LightCacheInside::get("static_route", $route_path)) === null) {
ctx()->getResponse()->endWithStatus(404);
return false;
}
unset($params["_route"]);
$obj = array_shift($params);
return new StaticFileHandler($obj, $path);
}
}

View File

@@ -16,17 +16,11 @@ class LightCacheInside
public static $last_error = '';
public static function init() {
self::$kv_table["wait_api"] = new Table(3, 0);
self::$kv_table["wait_api"]->column("value", Table::TYPE_STRING, 65536);
self::$kv_table["connect"] = new Table(8, 0);
self::$kv_table["connect"]->column("value", Table::TYPE_STRING, 256);
$result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create();
if ($result === false) {
self::$last_error = '系统内存不足,申请失败';
return false;
} else {
return true;
}
self::createTable("wait_api", 3, 65536); //用于存协程等待的状态内容的
self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的
self::createTable("static_route", 64, 256);//用于存储
//self::createTable("worker_start", 2, 1024);//用于存启动服务器时的状态的
return true;
}
/**
@@ -62,4 +56,17 @@ class LightCacheInside
public static function unset(string $table, string $key) {
return self::$kv_table[$table]->del($key);
}
/**
* @param $name
* @param $size
* @param $str_size
* @throws ZMException
*/
private static function createTable($name, $size, $str_size) {
self::$kv_table[$name] = new Table($size, 0);
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

@@ -28,8 +28,10 @@ class ZMAtomic
self::$atomics[$k] = new Atomic($v);
}
self::$atomics["stop_signal"] = new Atomic(0);
self::$atomics["_int_is_reload"] = new Atomic(0);
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 < 10; ++$i) {
self::$atomics["_tmp_" . $i] = new Atomic(0);
}

View File

@@ -0,0 +1,80 @@
<?php
namespace ZM\Utils;
use ZM\API\CQ;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Framework;
use ZM\Requests\ZMRequest;
class MessageUtil
{
/**
* 下载消息中 CQ 码的所有图片,通过 url
* @param $msg
* @param null $path
* @return array|false
*/
public static function downloadCQImage($msg, $path = null) {
$path = $path ?? DataProvider::getDataFolder() . "images/";
if (!is_dir($path)) mkdir($path);
$path = realpath($path);
if ($path === false) {
Console::warning("指定的路径错误不存在!");
return false;
}
$files = [];
$cq = CQ::getAllCQ($msg, true);
foreach ($cq as $v) {
if ($v->type == "image") {
$result = ZMRequest::downloadFile($v->params["url"], $path . "/" . $v->params["file"]);
if ($result === false) {
Console::warning("图片 " . $v->params["url"] . " 下载失败!");
return false;
}
$files[] = $path . "/" . $v->params["file"];
}
}
return $files;
}
/**
* 检查消息中是否含有图片 CQ 码
* @param $msg
* @return bool
*/
public static function containsImage($msg) {
$cq = CQ::getAllCQ($msg, true);
foreach ($cq as $v) {
if ($v->type == "image") {
return true;
}
}
return false;
}
/**
* 通过本地地址返回图片的 CQ 码
* type == 0 : 返回图片的 base64 CQ 码
* type == 1 : 返回图片的 file://路径 CQ 码(路径必须为绝对路径)
* type == 2 : 返回图片的 http://xxx CQ 码(默认为 /images/ 路径就是文件对应所在的目录)
* @param $file
* @param int $type
* @return string
*/
public static function getImageCQFromLocal($file, $type = 0) {
switch ($type) {
case 0:
return CQ::image("base64://" . base64_encode(file_get_contents($file)));
case 1:
return CQ::image("file://" . $file);
case 2:
$info = pathinfo($file);
return CQ::image(ZMConfig::get("global", "http_reverse_link") . "/images/" . $info["basename"]);
}
return "";
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace ZM\Utils;
use ZM\Console\Console;
class TaskManager
{
public static function runTask($task_name, $timeout = -1, ...$params) {
if (!isset(server()->setting["task_worker_num"])) {
Console::warning("未开启 TaskWorker 进程,请先修改 global 配置文件启用!");
return false;
}
$r = server()->taskwait(["task" => $task_name, "params" => $params], $timeout);
return $r === false ? false : $r["result"];
}
}

View File

@@ -11,13 +11,16 @@ use Swoole\Timer;
use ZM\Console\Console;
use ZM\Store\LightCache;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf;
class ZMUtil
{
public static function stop() {
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);
@@ -31,6 +34,12 @@ class ZMUtil
}
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) {

View File

@@ -5,7 +5,6 @@ use ZM\Utils\DataProvider;
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", ZMConfig::get("global", "zm_data"));
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
define("APP_VERSION", LOAD_MODE == 1 ? (json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown") : "unknown");
define("CRASH_DIR", ZMConfig::get("global", "crash_dir"));
@mkdir(ZM_DATA);

View File

@@ -261,19 +261,33 @@ function zm_yield() { Co::yield(); }
function zm_resume(int $cid) { Co::resume($cid); }
function zm_timer_after($ms, callable $callable) {
go(function () use ($ms, $callable) {
Swoole\Timer::after($ms, $callable);
Swoole\Timer::after($ms, function () use ($callable) {
call_with_catch($callable);
});
}
function call_with_catch($callable) {
try {
$callable();
} catch (Exception $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Uncaught " . get_class($e) . " when calling \"message\": " . $error_msg);
Console::trace();
}
}
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, $callable);
Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
});
} else {
return Swoole\Timer::tick($ms, $callable);
return Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
}
}