Compare commits

...

18 Commits

Author SHA1 Message Date
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
jerry
e977b09e20 Merge remote-tracking branch 'origin/master' 2021-03-02 21:24:53 +08:00
jerry
4ff75cf199 update to 2.2.8 version
update motd message
2021-03-02 21:24:31 +08:00
Whale
24e70c70ce Update deploy-docs.yml 2021-03-02 14:27:55 +08:00
Whale
275a7bf00b Update deploy-docs.yml 2021-03-02 14:26:02 +08:00
Whale
455fc79818 Update deploy-docs.yml 2021-03-02 14:22:40 +08:00
Whale
8740c3c255 Update deploy-docs.yml 2021-03-02 14:19:51 +08:00
Whale
98bfca5bb9 Update deploy-docs.yml 2021-03-02 14:18:40 +08:00
Whale
fc8d01ad5f Update deploy-docs.yml 2021-03-02 13:53:12 +08:00
Whale
d9b8df1725 Update deploy-docs.yml 2021-03-02 13:50:52 +08:00
Whale
9b7802ac04 Update deploy-docs.yml 2021-03-02 13:50:39 +08:00
Whale
6e1f3e0406 Update deploy-docs.yml 2021-03-02 13:43:45 +08:00
Whale
a2d4bab062 Update index.md 2021-03-02 13:40:21 +08:00
Whale
f1cefad910 Create deploy-docs.yml 2021-03-02 13:37:07 +08:00
22 changed files with 560 additions and 118 deletions

3
.gitignore vendored
View File

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

View File

@@ -1,14 +1,6 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php /** @noinspection PhpIncludeInspection */
if (!is_dir(__DIR__ . '/../vendor')) { require_once ((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . "/..")) . "/vendor/autoload.php";
define("LOAD_MODE", 1); //composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
/** @noinspection PhpIncludeInspection */
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
} else {
define("LOAD_MODE", 0); //源码模式
require_once __DIR__ . "/../vendor/autoload.php";
}
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run(); (new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run();

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,9 @@
{ {
"name": "zhamao/framework", "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", "minimum-stability": "stable",
"license": "Apache-2.0", "license": "Apache-2.0",
"version": "2.2.7", "version": "2.2.10",
"extra": { "extra": {
"exclude_annotate": [ "exclude_annotate": [
"src/ZM" "src/ZM"
@@ -11,12 +11,8 @@
}, },
"authors": [ "authors": [
{ {
"name": "whale", "name": "jerry",
"email": "crazysnowcc@gmail.com" "email": "admin@zhamao.me"
},
{
"name": "swift",
"email": "hugo_swift@yahoo.com"
} }
], ],
"prefer-stable": true, "prefer-stable": true,
@@ -52,7 +48,6 @@
] ]
}, },
"require-dev": { "require-dev": {
"swoole/ide-helper": "@dev", "swoole/ide-helper": "@dev"
"phpunit/phpunit": "^9.5"
} }
} }

View File

@@ -27,7 +27,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */ /** 对应swoole的server->set参数 */
$config['swoole'] = [ $config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log', '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 'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'max_coroutine' => 300000, 'max_coroutine' => 300000,
//'task_worker_num' => 4, //'task_worker_num' => 4,

View File

@@ -1,6 +1,6 @@
______ ______
|__ / |__ __ _ _ __ ___ __ _ ___ |__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \ / /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) | / /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/ /____|_| |_|\__,_|_| |_| |_|\__,_|\___/

View File

@@ -1 +1,3 @@
# FAQ # FAQ
这里会写一些常见的疑难解答。

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") * @CQCommand("测试fd")
*/ */
public function testfd() { 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> <chat-box>
) test abc 334 argtest ) test abc 334 argtest
( 参数内容abc, 334, argtest ( 参数内容abc, 334, argtest
</chat-box> </chat-box>

View File

@@ -34,4 +34,21 @@ DataProvider 是框架内提供的一个简易的文件管理类。
定义:`loadFromJson($filename)` 定义:`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

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

View File

@@ -8,14 +8,13 @@
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档 编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能 框架主要用途为 HTTP/WS 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
在 HTTP 和 WebSocket 服务器上PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。 在 HTTP 和 WebSocket 服务器上PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快,未来也会兼容微信公众号开发者模式 此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
```php ```php
/** /**
@@ -39,9 +38,9 @@ public function index() {
首先,你需要了解你需要知道哪些事情才能开始着手使用框架: 首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行(会跑 Linux 程序) 1. Linux 命令行(会跑 Linux 程序)
2. php 7.2+ 开发环境 2. php 7.2+ 开发环境(项目会持续支持最新的 PHP 版本)
3. HTTP 协议(可选) 3. HTTP/WebSocket 协议
4. OneBot 机器人聊天接口标准(可选) 4. OneBot 机器人聊天接口标准
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。 需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。

View File

@@ -1,5 +1,27 @@
# 更新日志v2 版本) # 更新日志v2 版本)
## 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
- 更新MOTD 显示的方式,更加直观和炫酷
## v2.2.7 ## v2.2.7
> 更新时间2021.2.27 > 更新时间2021.2.27
@@ -7,7 +29,6 @@
- 修复2.2.6 版本下 `reply()` 方法在群里调用会 at 成员的 bug - 修复2.2.6 版本下 `reply()` 方法在群里调用会 at 成员的 bug
- 修复:空 `access_token` 的情况下会无法连入的 bug - 修复:空 `access_token` 的情况下会无法连入的 bug
- 修复:使用 Closure 闭包函数自行编写逻辑的判断返回 false 无法阻断连接的 bug - 修复:使用 Closure 闭包函数自行编写逻辑的判断返回 false 无法阻断连接的 bug
-
## v2.2.6 ## v2.2.6

View File

@@ -88,6 +88,7 @@ nav:
- 全局方法: component/global-functions.md - 全局方法: component/global-functions.md
- HTTP 和 WebSocket 客户端: component/zmrequest.md - HTTP 和 WebSocket 客户端: component/zmrequest.md
- Console 终端: component/console.md - Console 终端: component/console.md
- Token 验证: component/access-token.md
- 进阶开发: - 进阶开发:
- 进阶开发: advanced/index.md - 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md - 框架剖析: advanced/framework-structure.md

View File

@@ -26,6 +26,7 @@ class RunServerCommand extends Command
new InputOption("disable-coroutine", null, null, "关闭协程Hook"), new InputOption("disable-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"), new InputOption("daemon", null, null, "以守护进程的方式运行框架"),
new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"), new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"),
new InputOption("show-php-ver", null, null, "启动时显示PHP版本"),
new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"), new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"),
]); ]);
$this->setDescription("Run zhamao-framework | 启动框架"); $this->setDescription("Run zhamao-framework | 启动框架");
@@ -39,7 +40,6 @@ class RunServerCommand extends Command
return Command::FAILURE; return Command::FAILURE;
} }
} }
if (LOAD_MODE == 0) echo "* This is repository mode.\n";
(new Framework($input->getOptions()))->start(); (new Framework($input->getOptions()))->start();
return Command::SUCCESS; return Command::SUCCESS;
} }

View File

@@ -26,23 +26,21 @@ class ConsoleApplication extends Application
public function initEnv() { public function initEnv() {
$this->selfCheck(); $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) $this->add(new BuildCommand()); //只有在git源码模式才能使用打包指令
if (LOAD_MODE === 0) define("WORKING_DIR", getcwd()); if (LOAD_MODE === 0) define("WORKING_DIR", getcwd());
elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../")); elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../"));
elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL;
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) { if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
/** @noinspection PhpIncludeInspection */ /** @noinspection PhpIncludeInspection */
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php"; require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
} }
if (LOAD_MODE == 2) { if (LOAD_MODE == 0) {
// Phar 模式2.0 不提供哦
//require_once FRAMEWORK_DIR . "/vendor/autoload.php";
spl_autoload_register('phar_classloader');
} elseif (LOAD_MODE == 0) {
/** @noinspection PhpIncludeInspection
* @noinspection RedundantSuppression
*/
require_once WORKING_DIR . "/vendor/autoload.php";
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true); $composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) { if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] "; echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";

View File

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

View File

@@ -80,11 +80,16 @@ class ServerEventHandler
}); });
} }
Process::signal(SIGINT, function () use ($r) { Process::signal(SIGINT, function () use ($r) {
echo "\r"; if (zm_atomic("_int_is_reload")->get() === 1) {
Console::warning("Server interrupted(SIGINT) on Master."); zm_atomic("_int_is_reload")->set(0);
if ((Framework::$server->inotify ?? null) !== null) ZMUtil::reload();
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify); } else {
ZMUtil::stop(); 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"]) { if (Framework::$argv["daemon"]) {
$daemon_data = json_encode([ $daemon_data = json_encode([

View File

@@ -9,6 +9,7 @@ use Exception;
use ZM\Annotation\Swoole\OnSetup; use ZM\Annotation\Swoole\OnSetup;
use ZM\Config\ZMConfig; use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM; use ZM\ConnectionManager\ManagerGM;
use ZM\Console\TermColor;
use ZM\Event\ServerEventHandler; use ZM\Event\ServerEventHandler;
use ZM\Store\LightCache; use ZM\Store\LightCache;
use ZM\Store\LightCacheInside; use ZM\Store\LightCacheInside;
@@ -73,8 +74,7 @@ class Framework
die($e->getMessage()); die($e->getMessage());
} }
try { try {
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
$this->server_set = ZMConfig::get("global", "swoole");
Console::init( Console::init(
ZMConfig::get("global", "info_level") ?? 2, ZMConfig::get("global", "info_level") ?? 2,
self::$server, self::$server,
@@ -85,37 +85,44 @@ class Framework
$timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai"; $timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai";
date_default_timezone_set($timezone); date_default_timezone_set($timezone);
$this->server_set = ZMConfig::get("global", "swoole");
$this->parseCliArgs(self::$argv); $this->parseCliArgs(self::$argv);
$out = [ // 打印初始信息
"host" => ZMConfig::get("global", "host"), $out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port");
"port" => ZMConfig::get("global", "port"), if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
"log_level" => Console::getLevel(), else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"];
"version" => ZM_VERSION, $out["environment"] = $args["env"] === null ? "default" : $args["env"];
"config" => $args["env"] === null ? 'global.php' : $args["env"] $out["log_level"] = Console::getLevel();
]; $out["version"] = ZM_VERSION;
if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION; if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) { if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) {
$out["task_worker_num"] = ZMConfig::get("global", "swoole")["task_worker_num"]; $out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"];
} }
if (($num = ZMConfig::get("global", "swoole")["worker_num"] ?? swoole_cpu_num()) != 1) { if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
$out["worker_num"] = $num; $conf = ZMConfig::get("global", "sql_config");
$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");
$out["redis_pool"] = $conf["host"] . ":" . $conf["port"];
}
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["working_dir"] = DataProvider::getWorkingDir(); $out["working_dir"] = DataProvider::getWorkingDir();
Console::printProps($out, $tty_width); 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); self::$server->set($this->server_set);
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt"); self::printMotd($tty_width);
} else {
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
}
$motd = explode("\n", $motd);
foreach ($motd as $k => $v) {
$motd[$k] = substr($v, 0, $tty_width);
}
$motd = implode("\n", $motd);
echo $motd;
global $asd; global $asd;
$asd = get_included_files(); $asd = get_included_files();
// 注册 Swoole Server 的事件 // 注册 Swoole Server 的事件
@@ -167,10 +174,25 @@ class Framework
} catch (Exception $e) { } catch (Exception $e) {
Console::error("Framework初始化出现错误请检查"); Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage()); Console::error($e->getMessage());
Console::debug($e);
die; die;
} }
} }
private static function printMotd($tty_width) {
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
} else {
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
}
$motd = explode("\n", $motd);
foreach ($motd as $k => $v) {
$motd[$k] = substr($v, 0, $tty_width);
}
$motd = implode("\n", $motd);
echo $motd;
}
public function start() { public function start() {
self::$server->start(); self::$server->start();
} }
@@ -271,6 +293,7 @@ class Framework
Console::$theme = $y; Console::$theme = $y;
} }
break; break;
case 'show-php-ver':
default: default:
//Console::info("Calculating ".$x); //Console::info("Calculating ".$x);
//dump($y); //dump($y);
@@ -280,6 +303,92 @@ class Framework
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL); if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
} }
private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) {
$tmp_line = $k . ": " . $v;
//Console::info("写入[".$tmp_line."]");
if (strlen($tmp_line) >= $line_width[$current_line]) { //输出的内容太多了,以至于一行都放不下一个,要折行
$title_strlen = strlen($k . ": ");
$content_len = $line_width[$current_line] - $title_strlen;
$line_data[$current_line] = " " . $k . ": ";
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
$line_data[$current_line] .= substr($v, 0, $content_len);
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
$rest = substr($v, $content_len);
++$current_line; // 带标题的第一行满了,折到第二行
do {
if ($colorful) $line_data[$current_line] = TermColor::color8(32);
$line_data[$current_line] .= " " . substr($rest, 0, $max_border - 2);
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
$rest = substr($rest, $max_border - 2);
++$current_line;
} while ($rest > $max_border - 2); // 循环,直到放完
} else { // 不需要折行
//Console::info("不需要折行");
$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;
if ($max_border >= 57) {
if (strlen($tmp_line) >= intval(($max_border - 2) / 2)) { // 不需要折行,直接输出一个转下一行
//Console::info("不需要折行,直接输出一个转下一行");
++$current_line;
} else { // 输出很小,写到前面并分片
//Console::info("输出很小,写到前面并分片");
$space = intval($max_border / 2) - 2 - strlen($tmp_line);
$line_data[$current_line] .= str_pad("", $space, " ");
$line_data[$current_line] .= "| "; // 添加分片
$line_width[$current_line] -= (strlen($tmp_line) + 3 + $space);
}
} else {
++$current_line;
}
}
}
public static function printProps($out, $tty_width, $colorful = true) {
$max_border = $tty_width < 65 ? $tty_width : 65;
if (LOAD_MODE == 0) echo Console::setColor("* Framework started with source mode.\n", $colorful ? "yellow" : "");
echo str_pad("", $max_border, "=") . PHP_EOL;
$current_line = 0;
$line_width = [];
$line_data = [];
foreach ($out as $k => $v) {
start:
if (!isset($line_width[$current_line])) {
$line_width[$current_line] = $max_border - 2;
}
//Console::info("行宽[$current_line]".$line_width[$current_line]);
if ($max_border >= 57) { // 很宽的时候,一行能放两个短行
if ($line_width[$current_line] == ($max_border - 2)) { //空行
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
} else { // 不是空行,已经有东西了
$tmp_line = $k . ": " . $v;
//Console::info("[$current_line]即将插入后面的东西[".$tmp_line."]");
if (strlen($tmp_line) > $line_width[$current_line]) { // 地方不够,另起一行
$line_data[$current_line] = str_replace("| ", "", $line_data[$current_line]);
++$current_line;
goto start;
} else { // 地方够,直接写到后面并另起一行
$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;
}
}
} else { // 不够宽,直接写单行
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
}
}
foreach ($line_data as $v) {
echo $v . PHP_EOL;
}
echo str_pad("", $max_border, "=") . PHP_EOL;
}
public static function getTtyWidth() { public static function getTtyWidth() {
return explode(" ", trim(exec("stty size")))[1]; return explode(" ", trim(exec("stty size")))[1];
} }

View File

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

View File

@@ -11,13 +11,16 @@ use Swoole\Timer;
use ZM\Console\Console; use ZM\Console\Console;
use ZM\Store\LightCache; use ZM\Store\LightCache;
use ZM\Store\LightCacheInside; use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic; use ZM\Store\ZMAtomic;
use ZM\Store\ZMBuf; use ZM\Store\ZMBuf;
class ZMUtil class ZMUtil
{ {
public static function stop() { public static function stop() {
if (SpinLock::tryLock("_stop_signal") === false) return;
Console::warning(Console::setColor("Stopping server...", "red")); Console::warning(Console::setColor("Stopping server...", "red"));
if (Console::getLevel() >= 4) Console::trace();
LightCache::savePersistence(); LightCache::savePersistence();
if (ZMBuf::$terminal !== null) if (ZMBuf::$terminal !== null)
Event::del(ZMBuf::$terminal); Event::del(ZMBuf::$terminal);
@@ -31,6 +34,12 @@ class ZMUtil
} }
public static function reload($delay = 800) { 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")); Console::info(Console::setColor("Reloading server...", "gold"));
usleep($delay * 1000); usleep($delay * 1000);
foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) { foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) {

View File

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