mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-02 22:35:38 +08:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e77b9d4970 | |||
|
|
456b102c15 | ||
|
|
cc57997abc | ||
|
|
19e61c7cc3 | ||
|
|
f908513dca | ||
|
|
7dc39e6ada | ||
|
|
b0be53554d | ||
|
|
b98048bd39 | ||
|
|
fffd3fdc95 | ||
|
|
dea9ed2ccd | ||
|
|
de5744c9e4 | ||
|
|
a23f3d8f16 | ||
|
|
0c24bfdedd | ||
|
|
c0b95c6840 | ||
|
|
e977b09e20 | ||
|
|
4ff75cf199 | ||
|
|
24e70c70ce | ||
|
|
275a7bf00b | ||
|
|
455fc79818 | ||
|
|
8740c3c255 | ||
|
|
98bfca5bb9 | ||
|
|
fc8d01ad5f | ||
|
|
d9b8df1725 | ||
|
|
9b7802ac04 | ||
|
|
6e1f3e0406 | ||
|
|
a2d4bab062 | ||
|
|
f1cefad910 | ||
|
|
957c69bd1e | ||
|
|
2902c5e805 | ||
|
|
faf9f5d988 | ||
|
|
874f061468 | ||
|
|
69521a1f1f | ||
|
|
fb9dbed306 | ||
|
|
d42158ac90 | ||
|
|
ff3ebec562 | ||
|
|
ea79de617e | ||
|
|
9e1ad6a983 | ||
|
|
c17248df31 | ||
|
|
4c116ebd86 | ||
|
|
c490fe0c1c | ||
|
|
cefdf23799 | ||
|
|
7f70642606 | ||
|
|
1d5b2609f9 | ||
|
|
a206fe8b87 | ||
|
|
fb4f6c45ce | ||
|
|
c50ae245bd | ||
|
|
f6c2131ebf | ||
|
|
543d1d2922 | ||
|
|
bb61e6f6a2 | ||
|
|
2d1bbf6b48 | ||
|
|
67e42cfe3e | ||
|
|
429a2cf230 | ||
|
|
9ace85e604 | ||
|
|
f677b0e132 | ||
|
|
f137f044d0 | ||
|
|
77c12db31a | ||
|
|
b670cb29fe | ||
|
|
95d7bb071d | ||
|
|
eadb4c1dee | ||
|
|
6672a6c852 | ||
|
|
094feddda4 | ||
|
|
f86eddb298 | ||
|
|
a93b4917cd | ||
|
|
0f9767aa16 | ||
|
|
0c9f246690 | ||
|
|
517d258d61 | ||
|
|
61e3818563 | ||
|
|
776ec98a3e | ||
|
|
f3e844bb0a |
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ ! -d "/app/zhamao-framework/bin" ]; then
|
||||
cp -r /app/zhamao-framework-bak/* /app/zhamao-framework/
|
||||
fi
|
||||
php /app/zhamao-framework/bin/start
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,4 +10,5 @@ composer.lock
|
||||
/bin/.phpunit.result.cache
|
||||
/resources/zhamao.service
|
||||
.phpunit.result.cache
|
||||
.daemon_pid
|
||||
.daemon_pid
|
||||
/runtime/
|
||||
50
README.md
50
README.md
@@ -1,26 +1,24 @@
|
||||
<div align="center">
|
||||
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
|
||||
<img src="https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/logo_trans.png" width = "150" height = "150" alt="炸毛框架"><br>
|
||||
<h2>炸毛框架</h2>
|
||||
炸毛框架 (zhamao-framework) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
|
||||
|
||||
[]()
|
||||
[](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes)
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
|
||||
[](https://packagist.org/packages/zhamao/framework)
|
||||
[]()
|
||||
[](https://github.com/howmanybots/onebot)
|
||||
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
|
||||
|
||||
</div>
|
||||
|
||||
## 开发者注意
|
||||
**开发者 QQ 群:670821194**
|
||||
开发者 QQ 群:**670821194** [点击加入群聊](https://jq.qq.com/?_wv=1027&k=YkNI3AIr)
|
||||
|
||||
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
|
||||
当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!
|
||||
|
||||
**2.0 版本如果有问题请第一时间加群反馈!**
|
||||
|
||||
有关 3.0 版本的最新情况,请看这里:[Issue #22](https://github.com/zhamao-robot/zhamao-framework/issues/22)
|
||||
2.0 版本如果有问题请第一时间加群反馈!
|
||||
|
||||
## 简介
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
@@ -46,38 +44,44 @@ public function index() {
|
||||
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。
|
||||
|
||||
## 文档(v2 版本)
|
||||
查看文档:[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/)
|
||||
查看文档(国内自建):<https://docs-v2.zhamao.xin/>
|
||||
|
||||
备用链接:[https://docs-v2.zhamao.me/](https://docs-v2.zhamao.me/)
|
||||
备用链接(国外托管):<https://docs-v2.zhamao.me/>
|
||||
|
||||
自行构建文档:`mkdocs build -d distribute`
|
||||
|
||||
## 特点
|
||||
- 支持多账号
|
||||
- 原生为多账号设计,支持多个机器人负载均衡
|
||||
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
|
||||
- 灵活的注解事件绑定机制
|
||||
- 支持下断点调试(Psysh)
|
||||
- 易用的上下文,模块内随处可用
|
||||
- 采用模块化编写,可单独拆装功能
|
||||
- 常驻内存,全局缓存变量随处使用
|
||||
- 采用模块化编写,可自由搭配其他 composer 组件
|
||||
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
|
||||
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
|
||||
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
|
||||
- 静态文件服务器
|
||||
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
|
||||
- 静态文件服务器,可将前端合并到一起
|
||||
|
||||
## 从 v1 升级
|
||||
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
|
||||
|
||||
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
|
||||
|
||||
## 下载源码
|
||||
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和 clone 仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
|
||||
|
||||
例如:`git clone https://fgit.zhamao.me/zhamao-robot/zhamao-framework.git`。
|
||||
|
||||
## 贡献和捐赠
|
||||
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
|
||||
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
|
||||
|
||||
目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
|
||||
|
||||
本项目为作者闲暇时间开发,如果觉得好用,不妨进行捐助~你的捐助会让我更加有动力完善插件,感谢你的支持!
|
||||
|
||||
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。
|
||||
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
|
||||
|
||||
### 支付宝
|
||||

|
||||

|
||||
|
||||
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ,我们会作为您捐助了本项目!
|
||||
|
||||
@@ -86,7 +90,7 @@ public function index() {
|
||||
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
|
||||
|
||||
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ(627577391)或提交 Issue 进行疑难解答。
|
||||
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ([627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。
|
||||
|
||||
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
|
||||
|
||||
@@ -94,4 +98,6 @@ public function index() {
|
||||
|
||||
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
|
||||
|
||||
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
|
||||
|
||||

|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/** @noinspection ALL */<?php
|
||||
/**
|
||||
* Copyright: Swlib
|
||||
* Author: Twosee <twose@qq.com>
|
||||
@@ -52,6 +52,7 @@ if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
$starttime = microtime(true);
|
||||
go(function (){
|
||||
|
||||
12
bin/start
12
bin/start
@@ -1,14 +1,6 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
<?php /** @noinspection PhpIncludeInspection */
|
||||
|
||||
if (!is_dir(__DIR__ . '/../vendor')) {
|
||||
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";
|
||||
}
|
||||
require_once ((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . "/..")) . "/vendor/autoload.php";
|
||||
|
||||
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run();
|
||||
|
||||
@@ -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
226
build-runtime.sh
Executable 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\""
|
||||
@@ -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.1",
|
||||
"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,
|
||||
@@ -26,19 +21,18 @@
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"doctrine/annotations": "~1.10",
|
||||
"ext-json": "*",
|
||||
"ext-posix": "*",
|
||||
"doctrine/annotations": "~1.10",
|
||||
"psy/psysh": "@stable",
|
||||
"symfony/polyfill-ctype": "^1.20",
|
||||
"symfony/polyfill-mbstring": "^1.20",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/routing": "^5.1",
|
||||
"zhamao/connection-manager": "*@dev",
|
||||
"zhamao/console": "^1.0",
|
||||
"zhamao/config": "^1.0",
|
||||
"zhamao/request": "*@dev",
|
||||
"symfony/routing": "^5.1",
|
||||
"symfony/polyfill-php80": "^1.20",
|
||||
"ext-posix": "*"
|
||||
"zhamao/request": "*@dev"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "*",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# FAQ
|
||||
|
||||
这里会写一些常见的疑难解答。
|
||||
@@ -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`。
|
||||
|
||||
231
docs/advanced/example/admin.md
Normal file
231
docs/advanced/example/admin.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 编写管理员专属功能
|
||||
|
||||
众所周知,如果大家使用炸毛框架来开发聊天机器人的话,会比较方便。但是有些地方你一定会感觉还是欠缺了点,比如下面这样,你想编写一个只能由机器人管理员,也就是你自己,才能触发的功能:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
```
|
||||
|
||||
这时候,如果只是自己有绝对的权利,可以将自己的 QQ 号写死在注解 `@CQCommand` 中,并限定 `user_id`(假设我的 QQ 号码为 123456):
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group",user_id=123456)
|
||||
*/
|
||||
```
|
||||
|
||||
但是,随着时间的推移,你的机器人伙伴群可能越来越大,这个命令可能不止需要绝对的你来使用,你还要将机器人的部分权利下发给更多的伙伴,怎么办呢?注解里面只能写死的。
|
||||
|
||||
答案很简单,这时候我们就需要用到框架提供的中间件(Middleware)。中间件说白了就是在事件执行前、后、过程中抛出的异常对其进行阻断和插入代码,比如我们上方在触发禁言这个注解事件前首先要判断执行这个命令的是不是钦定的管理员。
|
||||
|
||||
## 第一步:定义中间件
|
||||
|
||||
首先,我们需要定义一个中间件。在框架默认提供的脚手架中,包含了一个叫 `TimerMiddleware.php` 的示例中间件,这个示例中间件的目的是非常简单的,就是判断这个注解事件运行了多长时间。假设你有一个机器人功能,这个功能下的代码需要执行很长时间,可以使用这一注解轻松将事件执行的时间打印到终端上。
|
||||
|
||||
关于中间件的有关说明,见 [中间件](/event/middleware)。
|
||||
|
||||
下面我们假设你已经阅读过中间件注解的文档了,我们着手编写一个判断指令执行者是否是指定的管理员 QQ 的中间件。为了省事和让大家方便地复现,我先在脚手架下的目录 `src/Module/Middleware/` 下新建 PHP 类文件 `AdminMiddleware.php`(和 `TimerMiddleware.php` 在同一个目录)。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Middleware;
|
||||
|
||||
use ZM\Annotation\Http\HandleBefore;
|
||||
use ZM\Annotation\Http\MiddlewareClass;
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Http\MiddlewareInterface;
|
||||
use ZM\Store\LightCache;
|
||||
|
||||
/**
|
||||
* Class AdminMiddleware
|
||||
* 示例中间件:用于动态管理一些管理员指令的中间件
|
||||
* @package Module\Middleware
|
||||
* @MiddlewareClass("admin")
|
||||
*/
|
||||
class AdminMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @HandleBefore()
|
||||
* @return bool
|
||||
* @throws ZMException
|
||||
*/
|
||||
public function onBefore(): bool {
|
||||
$r = ctx()->getUserId(); // 从上下文获取发消息的用户 QQ
|
||||
$admin_list = LightCache::get("admin_list") ?? []; // 从轻量缓存获取管理员列表
|
||||
return in_array($r, $admin_list); // 返回这个 QQ 是否在管理员列表中
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中,`@MiddlewareClass("admin")` 的意思是,定义这个类为名字叫 `admin` 的中间件,同时,所有中间件的类**必须**带上 `implements MiddlewareInterface`,统一接口形式。
|
||||
|
||||
`@HandleBefore()` 代表的是,这个类下的这个函数(onBefore)被标注为这个中间件的 `onBefore` 事件,也就是说,如果有别的注解事件插入了这个 `admin` 中间件,那么执行对应注解事件前都要执行一下 `@HandleBefore` 所绑定的这个函数。而这个绑定的函数只能返回 `bool` 类型的值哦!
|
||||
|
||||
## 第二步:使用中间件
|
||||
|
||||
使用中间件很简单,在需要阻断的注解事件绑定的函数上再加一个注解就好了!我们以上方的禁言例子说明:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @Middleware("admin")
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我是管理员
|
||||
) 禁言 1234567 600
|
||||
( 禁言成功!
|
||||
^ 假设我不在管理员名单里
|
||||
) 禁言 1234567 900
|
||||
^ 机器人没有回复,因为中间件返回了 false,不继续执行
|
||||
</chat-box>
|
||||
|
||||
而这时候有朋友又要问了,如果我有一系列管理员命令,假设都在一个叫 `AdminFunc.php` 的模块类里,我是不是还得一个一个地给注解事件写 `@Middleware("admin")` 呢?当然不需要!如果你这个类所有的注解事件都是机器人的聊天事件(`@CQCommand`,`@CQMessage`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
// ...这里是你的一堆注解事件的函数
|
||||
}
|
||||
```
|
||||
|
||||
## 第三步:补全代码
|
||||
|
||||
上面我们讲到了,中间件里面使用了 `LightCache` 轻量缓存来储存临时的管理员列表,那么我们将这部分的代码完善吧!
|
||||
|
||||
=== "src/Module/Example/AdminFunc.php"
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\API\CQ;
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="解除禁言",message_type="group")
|
||||
*/
|
||||
public function unbanSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0);
|
||||
return "解除禁言成功!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "src/Module/Example/AdminManager.php"
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
|
||||
class AdminManager
|
||||
{
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
if (!LightCache::isset("admin_list")) { //一次性代码,首次执行才会执行if
|
||||
LightCache::set("admin_list", [ // 框架启动时初始化管理员列表
|
||||
"123456",
|
||||
"234567"
|
||||
], -2); // 这里用 -2 的原因是将这一列表持久化保存,避免关闭框架后丢失
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="添加管理员")
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
public function addAdmin() { //只有管理员才能添加管理员
|
||||
$qq = ctx()->getNextArg("请输入要添加管理员的QQ(qq号码,不可at)");
|
||||
SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
$ls = LightCache::get("admin_list");
|
||||
if (!in_array($qq, $ls)) $ls[] = $qq;
|
||||
LightCache::set("admin_list", $ls, -2);
|
||||
SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
return "成功添加 $qq 到管理员列表!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 现在我是 123456
|
||||
) 禁言 13579 60
|
||||
( 禁言成功!
|
||||
) 解除禁言 13579
|
||||
( 解除禁言成功!
|
||||
) 添加管理员 98765
|
||||
( 成功添加 98765 到管理员列表!
|
||||
^ 现在我是98765
|
||||
) 禁言 13579
|
||||
( 请输入禁言的时间(秒)
|
||||
) 120
|
||||
( 禁言成功!
|
||||
</chat-box>
|
||||
|
||||
70
docs/advanced/multi-process.md
Normal file
70
docs/advanced/multi-process.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 框架多进程
|
||||
|
||||
首先对于多进程概念,对于传统 PHP 程序员可能比较陌生,唯一接触到的地方可能就是 php-fpm 等一些方式处理时间长的请求时开进程去执行。关于多进程,我觉得廖雪峰的 Python 多进程这段讲的不错:
|
||||
|
||||
> Unix/Linux 操作系统提供了一个`fork()`系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是`fork()`调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
|
||||
|
||||
这里面的重点在于,多进程的创建,是父进程的复制,然后两个进程接下来运行的代码和存的内容就分道扬镳了。
|
||||
|
||||
PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采用多进程呢?
|
||||
|
||||
## 作用
|
||||
|
||||
使用过框架的你一定知道,框架是以命令行方式运行 PHP 的,而命令行方式运行 PHP,就代表要常驻内存,就像 Python、Node.js 一样。而默认情况下,比如 Python 的 Flask 为单线程单进程模式,也就是说同时只能处理一个 Web 请求。但大部分情况下,比如 Node.js,提供的都是异步 I/O,这也就是说明它在 Web 处理请求上,可同时承接的 I/O 密集型请求会更多一些,这样在对一般的 Web 应用中 I/O 密集型场景非常有用,而且往往只需要单进程也可以承载上万的并发请求。
|
||||
|
||||
在炸毛框架中,因为框架基于 Swoole 构建,所以天然支持协程,而协程就是针对 I/O 操作进行一个调度,类似异步的 Node.js,所以针对项目中存在太多的 SQL 语句执行、文件读写的话,炸毛框架直接上手,无需做任何修改,也可以达到很好的性能。
|
||||
|
||||
**但是**,CPU 密集型的应用怎么办呢?假设我的 Web 应用有大量的排序、md5 运算怎么办呢?这样的阻塞,假设是一个超级大的 for 循环或者是要执行很长时间的 while 循环,CPU 一直在被占用。多进程就是针对 CPU 密集型的应用说 yes 的一个方案。
|
||||
|
||||

|
||||
|
||||
我们假设现在有 3 个请求同时访问,也就是说上面的流程需要执行 3 遍。而如果我们只有一个进程的话,最后一个请求需要等待的时间为 `2*3+5*3=21` 秒,非常耗时。
|
||||
|
||||
而如果有两个进程处理 3 个请求,则最后一个完成的请求就缩短了,`2+5+2+5=14` 秒。
|
||||
|
||||
.png)
|
||||
|
||||
所以如果要充分利用你的服务器或者个人电脑的多核 CPU 资源,就要设置多个进程来处理。一个进程只能在一个 CPU 上运行,而设置了多进程后,就可以让多核 CPU 充分运行多个进程,所以我们给框架设置多进程的推荐数值为等同于 CPU 的核心数。
|
||||
|
||||
## 为什么不是多线程
|
||||
|
||||
因为众所周知,PHP 对线程的支持比较不好,而 ZTS 版本的 PHP 又会影响传统的 Web 端 PHP 的性能,再加上 Linux 对线程的切换效率和多进程切换的效率差不多,多线程容易造成数据读写不安全等问题,故 Swoole 使用的是多进程模型。
|
||||
|
||||
## 框架进程模型
|
||||
|
||||
.png)
|
||||
|
||||
上图中,横向的时间片可以理解为并行执行,这些操作在多个 CPU 内可能同时在执行。
|
||||
|
||||
## 进程间隔离
|
||||
|
||||
众所周知,进程是程序在操作系统中的一个边界,和自己有关的一切变量、内容和代码都在自己的进程内,不同进程之间如果不使用管道等方式,是不可以互相访问的。而加上开始描述的,创建子进程是一个复制自身的过程,所以也就会有如下图的情况:
|
||||
|
||||
.png)
|
||||
|
||||
我们以静态类为例,设置一个进程中的全局变量。这里就会出现,同一个静态变量在多个进程中完全不同的值的结果。此后,我们将会在 Worker 进程中执行用户的代码,如果设置 Worker 数量仅为 1 的话,那么就简单许多了,你还是可以使用全局变量或静态类来存储你想要的内容而不用担心这种多个进程变量隔离的情况(因为用户的 Web 请求处理的代码只会在一个 Worker 进程中执行)。如果像上图一样设置了多个 Worker,则用户过来的比如 HTTP 请求就有可能出现在不同的 Worker 进程中,给全局变量设值就一定会造成不同步的问题。这时我们就不可以使用全局变量做数据同步(注意,我说的是数据同步)。
|
||||
|
||||
## 跨进程同步
|
||||
|
||||
跨进程同步方案中,框架给出了很多种解决方案。
|
||||
|
||||
- MySQL 数据库
|
||||
- Redis
|
||||
- LightCache 轻量缓存(共享内存)
|
||||
- WorkerCache 大缓存
|
||||
- ZMAtomic 跨进程原子计数器
|
||||
|
||||
下面的表格我将列出下方的特点和各自的优缺点:
|
||||
|
||||
| 类型 | 用途 | 优点 | 缺点 |
|
||||
| ----------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| MySQL | 大型的传统的关系式数据都可以用数据库,你懂的 | 就是数据库的优点 | 和数据库不在同一台服务器的话网络延迟会较大,数据获取效率不高 |
|
||||
| Redis | 传统的 key-value 数据库 | 数据无同步等问题,性能高 | 有网络通信延迟 |
|
||||
| LightCache | 框架封装的跨进程的 key-value 存储模型 | 性能强悍,无 I/O 和网络通信 | 需要提前分配最大内存大小,最大单个值长度大小,不灵活 |
|
||||
| WorkerCache | 框架封装的基于进程的 key-value 存储模型,类似 Redis | 无需提前分配最大内存大小,受限于 PHP memory_limit | 见 WorkerCache 的说明 |
|
||||
|
||||
!!! note "WorkerCache 的说明"
|
||||
对于 WorkerCache 来说,其实是比较特殊的进程间通信。具体来说就是,WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
|
||||
|
||||
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。
|
||||
|
||||
BIN
docs/assets/img/Untitled Diagram (2).png
Normal file
BIN
docs/assets/img/Untitled Diagram (2).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
BIN
docs/assets/img/Untitled Diagram (3).png
Normal file
BIN
docs/assets/img/Untitled Diagram (3).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
docs/assets/img/Untitled Diagram (4).png
Normal file
BIN
docs/assets/img/Untitled Diagram (4).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/assets/img/single-process.png
Normal file
BIN
docs/assets/img/single-process.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
59
docs/component/access-token.md
Normal file
59
docs/component/access-token.md
Normal 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` 的相关章节。
|
||||
@@ -25,7 +25,7 @@ vendor/bin/start server --log-error # 以 error 等级启动框架
|
||||
vendor/bin/start server --log-warning # 以 warning 等级启动框架
|
||||
vendor/bin/start server --log-info # 以 info 等级启动框架
|
||||
vendor/bin/start server --log-verbose # 以 verbose 等级启动框架
|
||||
vendor/bin/start server --log-debug # 以 debug 等级 启动框架
|
||||
vendor/bin/start server --log-debug # 以 debug 等级启动框架
|
||||
```
|
||||
|
||||
## 使用 Log 输出内容
|
||||
@@ -100,11 +100,9 @@ $str = Console::setColor("I am gold color.", "gold");
|
||||
|
||||
炸毛框架支持从终端输入命令来进行一些操作,例如重启框架、停止框架、执行函数等。
|
||||
|
||||
::: warning 注意
|
||||
!!! warning 注意
|
||||
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
|
||||
:::
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
|
||||
### reload
|
||||
|
||||
@@ -160,6 +158,8 @@ color green 我是绿色的字
|
||||
|
||||
文件位置:`config/motd.txt`
|
||||
|
||||
其中,默认的 `Zhamao` 字样的 MOTD 是使用 **figlet** 命令生成的,`figlet "Zhamao"`,你也可以针对自己的机器人名称或品牌进行生成。
|
||||
|
||||
## 设置输出主题
|
||||
|
||||
Console 组件支持为多种不同的终端设置不同的主题,比如有些人喜欢使用白色的终端,但是白色终端下 info 的颜色很浅,看不到,还有人使用不能显示颜色的黑白终端.....
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -82,15 +82,20 @@ class Hello {
|
||||
|
||||
CQ 码字符反转义。
|
||||
|
||||
定义:`CQ::encode($msg, $is_content = false)`
|
||||
|
||||
当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。
|
||||
|
||||
| 反转义前 | 反转义后 |
|
||||
| -------- | -------- |
|
||||
| `&` | `&` |
|
||||
| `[` | `[` |
|
||||
| `]` | `]` |
|
||||
| `,` | `,` |
|
||||
|
||||
```php
|
||||
$str = CQ::decode("[我只是一条普通的文本]");
|
||||
// 转换为 "[我只是一条普通的文本]"
|
||||
$str = CQ::decode("[CQ:at,qq=我只是一条普通的文本]");
|
||||
// 转换为 "[CQ:at,qq=我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
### CQ::encode()
|
||||
@@ -102,6 +107,14 @@ $str = CQ::encode("[CQ:我只是一条普通的文本]");
|
||||
// $str: "[CQ:我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
定义:`CQ::encode($msg, $is_content = false)`
|
||||
|
||||
当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。
|
||||
|
||||
### CQ::escape()
|
||||
|
||||
同 `CQ::encode()`。
|
||||
|
||||
### CQ::removeCQ()
|
||||
|
||||
去除字符串中所有的 CQ 码。
|
||||
@@ -111,6 +124,53 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
|
||||
// $str: "这是带表情的全体消息"
|
||||
```
|
||||
|
||||
### CQ::getCQ()
|
||||
|
||||
解析 CQ 码。
|
||||
|
||||
- 定义:`getCQ($msg, $is_object = false)`
|
||||
- 参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject` 对象,此对象的属性和下表相同。(2.3.0+ 版本可用)
|
||||
- 返回:`数组 | CQObject | null`,见下表。
|
||||
|
||||
| 键名 | 说明 |
|
||||
| ------ | ------------------------------------------------------------ |
|
||||
| type | CQ码类型,比如 `[CQ:at]` 中的 `at` |
|
||||
| params | 参数列表,比如 `[CQ:image,file=123.jpg,url=http://a.com/a.jpg]`,params 为 `["file" => "123","url" => "http://a.com/a.jpg"]` |
|
||||
| start | 此 CQ 码在字符串中的起始位置 |
|
||||
| end | 此 CQ 码在字符串中的结束位置 |
|
||||
|
||||
### CQ::getAllCQ()
|
||||
|
||||
定义:`CQ::getAllCQ($msg, $is_object = false)`
|
||||
|
||||
参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject[]` 对象数组,此对象的属性和上面的表格内相同。(2.3.0+ 版本可用)
|
||||
|
||||
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
|
||||
|
||||
```php
|
||||
CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
|
||||
/*
|
||||
[
|
||||
[
|
||||
"type" => "at",
|
||||
"params" => [
|
||||
"qq" => "123",
|
||||
],
|
||||
"start" => 0,
|
||||
"end" => 13,
|
||||
],
|
||||
[
|
||||
"type" => "at",
|
||||
"params" => [
|
||||
"qq" => "456",
|
||||
],
|
||||
"start" => 17,
|
||||
"end" => 30,
|
||||
],
|
||||
]
|
||||
*/
|
||||
```
|
||||
|
||||
## CQ 码列表
|
||||
|
||||
### CQ::face() - 发送 QQ 表情
|
||||
@@ -119,7 +179,7 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
|
||||
|
||||
定义:`CQ::face($id)`
|
||||
|
||||
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [炸毛框架 1.x 版本文档](https://docs-v1.zhamao.xin/face_list.html)。
|
||||
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](https://static.zhamao.me/face_id.html)。
|
||||
|
||||
```php
|
||||
/**
|
||||
@@ -449,11 +509,31 @@ public function xmlTest() {
|
||||
|
||||
发送 QQ 兼容的 JSON 多媒体消息。
|
||||
|
||||
定义:`CQ::json($data)`
|
||||
定义:`CQ::json($data, $resid = 0)`
|
||||
|
||||
参数同上,内含 JSON 字符串即可。
|
||||
|
||||
其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0,走小程序通道,填了走富文本通道发送。
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
|
||||
|
||||
### CQ::_custom() - 扩展自定义 CQ 码
|
||||
|
||||
用于兼容各类含有被支持的扩展 CQ 码,比如 go-cqhttp 的 `[CQ:gift]` 礼物类型。
|
||||
|
||||
定义:`CQ::_custom(string $type_name, array $params)`
|
||||
|
||||
| 参数名 | 说明 |
|
||||
| ----------- | --------------------------------------------------- |
|
||||
| `type_name` | CQ 码类型,如 `music`,`at` |
|
||||
| `params` | 发送的 CQ 码中的参数数组,例如 `["qq" => "123456"]` |
|
||||
|
||||
下面是一个例子:
|
||||
|
||||
```php
|
||||
CQ::_custom("at",["qq" => "123456","qwe" => "asd"]);
|
||||
// 返回:[CQ:at,qq=123456,qwe=asd]
|
||||
```
|
||||
|
||||
|
||||
54
docs/component/data-provider.md
Normal file
54
docs/component/data-provider.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 存储管理(文件)
|
||||
|
||||
DataProvider 是框架内提供的一个简易的文件管理类。
|
||||
|
||||
定义:`\ZM\Utils\DataProvider`
|
||||
|
||||
## DataProvider::getWorkingDir()
|
||||
|
||||
同 `working_dir()`。
|
||||
|
||||
## DataProvider::getFrameworkLink()
|
||||
|
||||
同 `ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
|
||||
|
||||
## DataProvider::getDataFolder()
|
||||
|
||||
获取配置项 `zm_data` 指定的目录。
|
||||
|
||||
## DataProvider::saveToJson()
|
||||
|
||||
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
|
||||
|
||||
定义:`saveToJson($filename, $file_array)`
|
||||
|
||||
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
|
||||
|
||||
这里只支持二级目录,不支持更多级的子目录。
|
||||
|
||||
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
|
||||
|
||||
## DataProvider::loadFromJson()
|
||||
|
||||
从 json 文件加载内容至变量。
|
||||
|
||||
定义:`loadFromJson($filename)`
|
||||
|
||||
文件名同上 `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()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。
|
||||
|
||||
@@ -227,5 +227,32 @@ bot()->sendPrivateMsg(123456, "你好啊!!");
|
||||
// 等同于 ZMRobot::getRandom()->sendPrivateMsg(123456, "你好啊!!");
|
||||
```
|
||||
|
||||
## zm_atomic()
|
||||
|
||||
获取计时器,效果同 `\ZM\Store\ZMAtomic::get($name)`。
|
||||
|
||||
定义:`zm_atmoic($name)`
|
||||
|
||||
## uuidgen()
|
||||
|
||||
> 2.2.5 版本起可用。
|
||||
|
||||
生成一个随机的 uuid,支持大写或小写。
|
||||
|
||||
定义:`uuidgen($uppercase = false)`
|
||||
|
||||
当 `$uppercase` 为 `true` 时,返回的 uuid 中字母都是大写。
|
||||
|
||||
## working_dir()
|
||||
|
||||
> 2.2.6 版本起可用。
|
||||
|
||||
获取框架运行的工作目录。例如你是从 `/root/framework-starter/` 目录启动的框架,`vendor/bin/start server`,那么 `working_dir()` 返回的就是 `/root/framework-starter`。(注意,返回的目录最后没有斜杠,请自行添加。)
|
||||
|
||||
## getAllFdByConnectType()
|
||||
|
||||
获取同类型的所有连接的描述符 ID。
|
||||
|
||||
定义:`getAllFdByConnectType(string $type = 'default'): array`
|
||||
|
||||
当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
|
||||
@@ -25,8 +25,8 @@
|
||||
```php
|
||||
/** 轻量字符串缓存,默认开启 */
|
||||
$config['light_cache'] = [
|
||||
'size' => 1024, //最多允许储存的条数(需要2的倍数)
|
||||
'max_strlen' => 16384, //单行字符串最大长度(需要2的倍数)
|
||||
'size' => 512, //最多允许储存的条数(需要2的倍数)
|
||||
'max_strlen' => 32768, //单行字符串最大长度(需要2的倍数)
|
||||
'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多)
|
||||
'persistence_path' => $config['zm_data'].'_cache.json',
|
||||
'auto_save_interval' => 900
|
||||
@@ -292,7 +292,7 @@ class Hello {
|
||||
* @CQCommand("set_store")
|
||||
*/
|
||||
public function setStorage() {
|
||||
$arg1 = ctx()->getFullArg("请输入要设置的内容名称");
|
||||
$arg1 = ctx()->getNextArg("请输入要设置的内容名称");
|
||||
$arg2 = ctx()->getFullArg("请输入要设置的内容");
|
||||
WorkerCache::set($arg1, $arg2);
|
||||
return "成功!";
|
||||
|
||||
@@ -31,7 +31,7 @@ SpinLock::unlock("foo");
|
||||
给信号量 `$key` 上锁。如果该信号量已经被上锁,则立刻返回 false。
|
||||
|
||||
```php
|
||||
SpinLock::lock("foo");
|
||||
SpinLock::trylock("foo");
|
||||
```
|
||||
|
||||
## 综合实例
|
||||
@@ -70,4 +70,4 @@ public function test() {
|
||||
|
||||
## 性能
|
||||
|
||||
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下,使用自旋锁和不适用自旋锁的测试成绩时间分别为:7.4s 和 6.9s。
|
||||
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下,使用自旋锁和不适用自旋锁的测试成绩时间分别为:7.4s 和 6.9s。
|
||||
|
||||
@@ -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()
|
||||
|
||||
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。
|
||||
@@ -246,7 +267,7 @@
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\Annotation\Swoole\OnOpenEvent;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
class Hello {
|
||||
|
||||
@@ -163,5 +163,5 @@ public function onThrowing(?Exception $e) {
|
||||
|
||||
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
|
||||
|
||||
`context()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。
|
||||
`ctx()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。
|
||||
|
||||
|
||||
@@ -11,10 +11,25 @@ QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理
|
||||
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
|
||||
|
||||
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand),`@CQMessage` 注解处理。
|
||||
|
||||
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
|
||||
|
||||
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
|
||||
|
||||
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
|
||||
|
||||
## 注解事件参照表
|
||||
|
||||
| 注解名称 | 类所在命名全称 | 作用 |
|
||||
| ------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ |
|
||||
| [`@CQBefore`](/event/robot-annotations/#cqbefore) | `\ZM\Annotation\CQBefore` | OneBot 各类事件前触发的,可当作事件过滤器使用 |
|
||||
| [`@CQAfter`](/event/robot-annotations/#cqafter) | `\ZM\Annotation\CQAfter` | OneBot 各类事件后触发的 |
|
||||
| [`@CQMessage`](/event/robot-annotations/#cqmessage) | `\ZM\Annotation\CQMessage` | OneBot 中消息类事件的触发(机器人消息)事件 |
|
||||
| [`@CQCommand`](/event/robot-annotations/#cqcommand) | `\ZM\Annotation\CQCommand` | OneBot 中消息类事件的触发(机器人消息)事件,但是被封装为指令型的,无需自己切割命令式 |
|
||||
| [`@CQNotice`](/event/robot-annotations/#cqnotice) | `\ZM\Annotation\CQNotice` | OneBot 中通知类事件的触发(机器人消息)事件 |
|
||||
| [`@CQRequest`](/event/robot-annotations/#cqrequest) | `\ZM\Annotation\CQRequest` | OneBot 中请求类事件的触发(机器人消息)事件,一般带有请求信息,可联动相关响应的 API 完成功能编写 |
|
||||
| [`@CQMetaEvent`](/event/robot-annotations/#cqmetaevent) | `\ZM\Annotation\CQMetaEvent` | OneBot 中涉及 OneBot 实现本身的一些和机器人事件无关的元事件,比如 WS 连接的心跳包 |
|
||||
|
||||
## CQMessage()
|
||||
|
||||
QQ 收到消息后触发的事件对应注解。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
!!! quote "开发提示"
|
||||
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不罗嗦。
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不啰嗦。
|
||||
|
||||
## 控制器和路由
|
||||
|
||||
@@ -228,4 +228,4 @@ public function staticImage($param) {
|
||||
}
|
||||
```
|
||||
|
||||
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
|
||||
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
|
||||
|
||||
@@ -4,55 +4,63 @@
|
||||
|
||||
!!! error "警告"
|
||||
|
||||
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将将敏感信息写入配置文件并提交至开源仓库!
|
||||
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将敏感信息写入配置文件并提交至开源仓库!
|
||||
|
||||
## 全局配置文件 global.php
|
||||
|
||||
框架的全局配置文件在 `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**
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------------------------- | ----------------------------------------------- | ---------------------------- |
|
||||
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 1024 |
|
||||
| `max_strlen` | 作为 value 字符串的最大长度 | 16384 |
|
||||
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 512 |
|
||||
| `max_strlen` | 作为 value 字符串的最大长度 | 32768 |
|
||||
| `hash_conflict_proportion` | Hash冲突率(越大越好,但是需要的内存更多) | 0.6 |
|
||||
| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` |
|
||||
| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 |
|
||||
|
||||
### 子表 worker_cache
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------- | --------------------------- | ------ |
|
||||
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
|
||||
|
||||
### 子表 **sql_config**
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
@@ -83,19 +91,13 @@
|
||||
| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` |
|
||||
| `document_index` | 默认索引的文件名列表 | `["index.html"]` |
|
||||
|
||||
### 子表 worker_cache
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------- | --------------------------- | ------ |
|
||||
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
|
||||
|
||||
## 多环境下的配置文件
|
||||
|
||||
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(production / development / staging)来编写配置文件。
|
||||
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(development / staging / production)来编写配置文件。
|
||||
|
||||
### 使用环境参数
|
||||
|
||||
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `production`,`staging`,`development`。
|
||||
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `development`,`staging`,`production`。
|
||||
|
||||
```bash
|
||||
vendor/bin/start server --env=development
|
||||
@@ -103,7 +105,7 @@ vendor/bin/start server --env=development
|
||||
|
||||
### 不同环境配置文件
|
||||
|
||||
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制或改名为 `global.development.php` 或 `global.production.php` 即可。
|
||||
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制并重命名为 `global.development.php` 或 `global.production.php` 即可。
|
||||
|
||||
### 优先级
|
||||
|
||||
@@ -146,4 +148,4 @@ $r = ZMConfig::get("example_a", "key1"); # $r == "value1"
|
||||
$time = ZMConfig::get("example_a", "starttime"); # $time == 服务器启动时间
|
||||
```
|
||||
|
||||
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json` 或 `example_a.production.php` 均可。
|
||||
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json` 或 `example_a.production.php` 均可。
|
||||
|
||||
@@ -82,8 +82,19 @@ cd zhamao-framework-starter
|
||||
./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出
|
||||
```
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
如果国内 Composer 下载过慢,可以使用阿里云的 Composer 镜像加速。
|
||||
```bash
|
||||
# 仅对当前的项目使用阿里云加速
|
||||
composer config repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
# 对全局的 Composer 使用阿里云加速
|
||||
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
```
|
||||
|
||||
|
||||
## 启动框架
|
||||
|
||||
本地环境启动方式:
|
||||
```bash
|
||||
cd zhamao-framework-starter
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。
|
||||
|
||||
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架的。
|
||||
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架。
|
||||
|
||||
## 机器人客户端
|
||||
|
||||
@@ -30,51 +30,6 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
|
||||
|
||||
由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`。
|
||||
|
||||
=== "config.json(旧格式)"
|
||||
|
||||
``` json hl_lines="2 3 30 31"
|
||||
{
|
||||
"uin": 你的QQ号,
|
||||
"password": "你的密码",
|
||||
"encrypt_password": false,
|
||||
"password_encrypted": "",
|
||||
"enable_db": true,
|
||||
"access_token": "",
|
||||
"relogin": {
|
||||
"enabled": true,
|
||||
"relogin_delay": 3,
|
||||
"max_relogin_times": 0
|
||||
},
|
||||
"ignore_invalid_cqcode": false,
|
||||
"force_fragmented": true,
|
||||
"heartbeat_interval": 0,
|
||||
"http_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 5700,
|
||||
"timeout": 0,
|
||||
"post_urls": {}
|
||||
},
|
||||
"ws_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 6700
|
||||
},
|
||||
"ws_reverse_servers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"reverse_url": "ws://127.0.0.1:20001/",
|
||||
"reverse_api_url": "",
|
||||
"reverse_event_url": "",
|
||||
"reverse_reconnect_interval": 3000
|
||||
}
|
||||
],
|
||||
"post_message_format": "string",
|
||||
"debug": false,
|
||||
"log_level": ""
|
||||
}
|
||||
```
|
||||
|
||||
=== "config.hjson(新格式)"
|
||||
|
||||
``` json hl_lines="3 5 81 84"
|
||||
@@ -193,6 +148,51 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
|
||||
}
|
||||
```
|
||||
|
||||
=== "config.json(旧格式)"
|
||||
|
||||
``` json hl_lines="2 3 30 31"
|
||||
{
|
||||
"uin": 你的QQ号,
|
||||
"password": "你的密码",
|
||||
"encrypt_password": false,
|
||||
"password_encrypted": "",
|
||||
"enable_db": true,
|
||||
"access_token": "",
|
||||
"relogin": {
|
||||
"enabled": true,
|
||||
"relogin_delay": 3,
|
||||
"max_relogin_times": 0
|
||||
},
|
||||
"ignore_invalid_cqcode": false,
|
||||
"force_fragmented": true,
|
||||
"heartbeat_interval": 0,
|
||||
"http_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 5700,
|
||||
"timeout": 0,
|
||||
"post_urls": {}
|
||||
},
|
||||
"ws_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 6700
|
||||
},
|
||||
"ws_reverse_servers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"reverse_url": "ws://127.0.0.1:20001/",
|
||||
"reverse_api_url": "",
|
||||
"reverse_event_url": "",
|
||||
"reverse_reconnect_interval": 3000
|
||||
}
|
||||
],
|
||||
"post_message_format": "string",
|
||||
"debug": false,
|
||||
"log_level": ""
|
||||
}
|
||||
```
|
||||
|
||||
其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT
|
||||
|
||||
## 第一次对话
|
||||
@@ -224,5 +224,14 @@ public function repeat() {
|
||||
|
||||
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。
|
||||
|
||||
<chat-box>
|
||||
) echo 你好啊
|
||||
( 你好啊
|
||||
) echo
|
||||
( 请输入你要回复的内容
|
||||
) 哦豁
|
||||
( 哦豁
|
||||
</chat-box>
|
||||
|
||||
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
|
||||
> 如果是从 v1.x 版本升级到 v2.x,[点我看升级指南](/advanced/to-v2/)。
|
||||
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
!!! tip "提示"
|
||||
|
||||
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request,以此来贡献此文档!
|
||||
|
||||
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
|
||||
框架主要用途为 HTTP/WS 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
|
||||
在 HTTP 和 WebSocket 服务器上,PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
|
||||
|
||||
此外,QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快,未来也会兼容微信公众号开发者模式。
|
||||
此外,QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
|
||||
|
||||
```php
|
||||
/**
|
||||
@@ -34,9 +38,9 @@ public function index() {
|
||||
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
|
||||
|
||||
1. Linux 命令行(会跑 Linux 程序)
|
||||
2. php 7.2+ 开发环境
|
||||
3. HTTP 协议(可选)
|
||||
4. OneBot 机器人聊天接口标准(可选)
|
||||
2. php 7.2+ 开发环境(项目会持续支持最新的 PHP 版本)
|
||||
3. HTTP/WebSocket 协议
|
||||
4. OneBot 机器人聊天接口标准
|
||||
|
||||
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ function setCookie(name, value) {
|
||||
var Days = 30;
|
||||
var exp = new Date();
|
||||
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
|
||||
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path=/";
|
||||
}
|
||||
|
||||
s_theme=getCookie("_theme");
|
||||
|
||||
@@ -1,5 +1,108 @@
|
||||
# 更新日志(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
|
||||
|
||||
- 更新:MOTD 显示的方式,更加直观和炫酷
|
||||
|
||||
## v2.2.7
|
||||
|
||||
> 更新时间:2021.2.27
|
||||
|
||||
- 修复:2.2.6 版本下 `reply()` 方法在群里调用会 at 成员的 bug
|
||||
- 修复:空 `access_token` 的情况下会无法连入的 bug
|
||||
- 修复:使用 Closure 闭包函数自行编写逻辑的判断返回 false 无法阻断连接的 bug
|
||||
|
||||
## v2.2.6
|
||||
|
||||
> 更新时间:2021.2.26
|
||||
|
||||
- 新增:`uuidgen()` 全局函数,快速生成 uuid
|
||||
- 修复:MySQL `rawQuery()` 在参数为非数组时会报 Warning 的 bug
|
||||
- 新增:示例模块的 API 示例:一言查询
|
||||
- 优化:删减部分无用代码
|
||||
- 更改:`ctx()->reply()` 方法改为调用隐藏方法:`.handle_quick_operation`
|
||||
- 修复:`ctx()->finalReply()` 一直以来的 bug(未阻断事件)
|
||||
- 新增:`access_token` 配置项支持闭包函数自行设计判断方式和逻辑
|
||||
- 新增:全局函数 `working_dir()`
|
||||
|
||||
## v2.2.5
|
||||
|
||||
> 更新时间:2021.2.20
|
||||
|
||||
- 新增:`saveToJson()` 和 `loadFromJson()` 方法(DataProvider 类)
|
||||
- 修复:`@OnSave` 注解事件无法工作的 bug
|
||||
- 调整:自定义计时器创建时的性能调优
|
||||
- 新增:WorkerCache 方法:`hasKey()`
|
||||
- 新增:SpinLock 方法:`transaction()`(直接在事务中上锁)
|
||||
- 新增:CQ 方法:`getAllCQ()`,`_custom()`(获取消息中的所有 CQ 码)
|
||||
- 修复:CQ 类中的部分 bug
|
||||
|
||||
## v2.2.4
|
||||
|
||||
> 更新时间:2021.2.7
|
||||
|
||||
- 修复:终端交互导致的 ssh 断掉后 CPU 占用过高的问题
|
||||
- 修复:WorkerCache 在缺少配置文件下工作异常的问题
|
||||
- 新增:全局函数:`zm_atomic()`
|
||||
|
||||
## v2.2.3
|
||||
|
||||
> 更新时间:2021.1.30
|
||||
|
||||
- 修复:waitMessage() 在 v2.2.2 版本中不可用的 bug
|
||||
- 修复:access_token 无效的问题
|
||||
|
||||
## v2.2.2
|
||||
|
||||
> 更新时间:2021.1.29
|
||||
|
||||
- 修复:模块文件错误时避免循环报错
|
||||
- 优化:代码结构
|
||||
- 修复:在不同进程时调用机器人 API 无法返回且报错的 bug
|
||||
- **修复:机器人无法连接的问题(2.1.6 ~ 2.2.1 受影响)**
|
||||
|
||||
## v2.2.1
|
||||
|
||||
> 更新时间:2021.1.29
|
||||
|
||||
22
mkdocs.yml
22
mkdocs.yml
@@ -10,8 +10,8 @@ theme:
|
||||
favicon: assets/favicon.png
|
||||
language: zh
|
||||
palette:
|
||||
primary: blue
|
||||
accent: blue
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
features:
|
||||
- navigation.tabs
|
||||
extra_javascript:
|
||||
@@ -34,7 +34,7 @@ extra:
|
||||
version:
|
||||
method: mike
|
||||
|
||||
copyright: 'Copyright © 2019 - 2020 CrazyBot Team <span class="tx-switch">
|
||||
copyright: 'Copyright © 2019 - 2021 CrazyBot Team <span class="tx-switch">
|
||||
<button data-md-color-scheme="default"><code>默认模式</code></button>
|
||||
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
|
||||
</span>
|
||||
@@ -72,21 +72,28 @@ 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
|
||||
- Redis 数据库: component/redis.md
|
||||
- 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
|
||||
@@ -95,6 +102,9 @@ 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
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace Custom\Command;
|
||||
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class CustomCommand extends Command
|
||||
{
|
||||
// the name of the command (the part after "bin/console")
|
||||
protected static $defaultName = 'custom';
|
||||
|
||||
protected function configure() {
|
||||
$this->setDescription("custom description | 自定义命令的描述字段");
|
||||
$this->addOption("failure", null, null, "以错误码为1返回结果");
|
||||
// ...
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
if ($input->getOption("failure")) {
|
||||
$output->writeln("<error>Hello error! I am wrong message.</error>");
|
||||
return Command::FAILURE;
|
||||
} else {
|
||||
$output->writeln("<comment>Hello world! I am successful message.</comment>");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php #plain
|
||||
<?php /** @noinspection PhpFullyQualifiedNameUsageInspection */ #plain
|
||||
|
||||
//这里写你的全局函数
|
||||
/**
|
||||
* @param callable $func
|
||||
* @param string $name
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
function pgo(callable $func, $name = "default") {
|
||||
\ZM\Utils\CoroutinePool::go($func, $name);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
@@ -11,6 +11,8 @@ use ZM\Console\Console;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Requests\ZMRequest;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
/**
|
||||
@@ -20,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)
|
||||
@@ -45,6 +55,18 @@ class Hello
|
||||
return "你好啊,我是由炸毛框架构建的机器人!";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个最基本的第三方 API 接口使用示例
|
||||
* @CQCommand("一言")
|
||||
*/
|
||||
public function hitokoto() {
|
||||
$api_result = ZMRequest::get("https://v1.hitokoto.cn/");
|
||||
if ($api_result === false) return "接口请求出错,请稍后再试!";
|
||||
$obj = json_decode($api_result, true);
|
||||
if ($obj === null) return "接口解析出错!可能返回了非法数据!";
|
||||
return $obj["hitokoto"] . "\n----「" . $obj["from"] . "」";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个简单随机数的功能demo
|
||||
* 问法1:随机数 1 20
|
||||
@@ -89,7 +111,7 @@ class Hello
|
||||
* @return string
|
||||
*/
|
||||
public function paramGet($param) {
|
||||
return "Hello, ".$param["name"];
|
||||
return "Hello, " . $param["name"];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,6 +135,7 @@ class Hello
|
||||
/**
|
||||
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
|
||||
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
|
||||
* @throws InterruptException
|
||||
*/
|
||||
public function onRequest() {
|
||||
EventDispatcher::interrupt();
|
||||
|
||||
@@ -24,7 +24,7 @@ class TimerMiddleware implements MiddlewareInterface
|
||||
* @HandleBefore()
|
||||
* @return bool
|
||||
*/
|
||||
public function onBefore() {
|
||||
public function onBefore(): bool {
|
||||
$this->starttime = microtime(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\API;
|
||||
|
||||
|
||||
use ZM\Console\Console;
|
||||
use ZM\Entity\CQObject;
|
||||
|
||||
class CQ
|
||||
{
|
||||
@@ -45,11 +48,11 @@ class CQ
|
||||
*/
|
||||
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:image,file=" . $file .
|
||||
"[CQ:image,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($flash ? ",type=flash" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -64,11 +67,11 @@ class CQ
|
||||
*/
|
||||
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:record,file=" . $file .
|
||||
"[CQ:record,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($magic ? ",magic=1" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -82,10 +85,10 @@ class CQ
|
||||
*/
|
||||
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:video,file=" . $file .
|
||||
"[CQ:video,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -121,7 +124,7 @@ class CQ
|
||||
* @return string
|
||||
*/
|
||||
public static function poke($type, $id, $name = "") {
|
||||
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]";
|
||||
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? (",name=".self::encode($name, true)) : "") . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +133,7 @@ class CQ
|
||||
* @return string
|
||||
*/
|
||||
public static function anonymous($ignore = 1) {
|
||||
return "[CQ:anonymous".($ignore != 1 ? ",ignore=0" : "")."]";
|
||||
return "[CQ:anonymous" . ($ignore != 1 ? ",ignore=0" : "") . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,10 +146,10 @@ class CQ
|
||||
*/
|
||||
public static function share($url, $title, $content = null, $image = null) {
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
else $c = ",content=" . self::encode($content, true);
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
|
||||
else $i = ",image=" . self::encode($image, true);
|
||||
return "[CQ:share,url=" . self::encode($url, true) . ",title=" . self::encode($title, true) . $c . $i . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,8 +162,21 @@ class CQ
|
||||
return "[CQ:contact,type=$type,id=$id]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送位置
|
||||
* @param $lat
|
||||
* @param $lon
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public static function location($lat, $lon, $title = "", $content = "") {
|
||||
|
||||
return "[CQ:location" .
|
||||
",lat=".self::encode($lat, true) .
|
||||
",lon=".self::encode($lon, true).
|
||||
($title != "" ? (",title=".self::encode($title, true)) : "") .
|
||||
($content != "" ? (",content=".self::encode($content, true)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,10 +209,13 @@ class CQ
|
||||
return " ";
|
||||
}
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
else $c = ",content=" . self::encode($content, true);
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:music,type=custom,url=" . $id_or_url . ",audio=" . $audio . ",title=" . $title . $c . $i . "]";
|
||||
else $i = ",image=" . self::encode($image, true);
|
||||
return "[CQ:music,type=custom,url=" .
|
||||
self::encode($id_or_url, true) .
|
||||
",audio=" . self::encode($audio, true) . ",title=" . self::encode($title, true) . $c . $i .
|
||||
"]";
|
||||
default:
|
||||
Console::warning("传入的music type($type)错误!");
|
||||
return " ";
|
||||
@@ -208,19 +227,36 @@ class CQ
|
||||
}
|
||||
|
||||
public static function node($user_id, $nickname, $content) {
|
||||
return "[CQ:node,user_id=$user_id,nickname=$nickname,content=".self::escape($content)."]";
|
||||
return "[CQ:node,user_id=$user_id,nickname=".self::encode($nickname, true).",content=" . self::encode($content, true) . "]";
|
||||
}
|
||||
|
||||
public static function xml($data) {
|
||||
return "[CQ:xml,data=" . self::encode($data, true) . "]";
|
||||
}
|
||||
|
||||
public static function json($data, $resid = 0) {
|
||||
return "[CQ:json,data=" . self::encode($data, true) . ",resid=" . intval($resid) . "]";
|
||||
}
|
||||
|
||||
public static function _custom(string $type_name, $params) {
|
||||
$code = "[CQ:" . $type_name;
|
||||
foreach ($params as $k => $v) {
|
||||
$code .= "," . $k . "=" . self::escape($v, true);
|
||||
}
|
||||
$code .= "]";
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义字符串中的CQ码敏感符号
|
||||
* @param $str
|
||||
* @param $msg
|
||||
* @param bool $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decode($str) {
|
||||
$str = str_replace("&", "&", $str);
|
||||
$str = str_replace("[", "[", $str);
|
||||
$str = str_replace("]", "]", $str);
|
||||
return $str;
|
||||
public static function decode($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
public static function replace($str) {
|
||||
@@ -230,42 +266,99 @@ class CQ
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义CQ码
|
||||
* 转义CQ码的特殊字符,同encode
|
||||
* @param $msg
|
||||
* @param bool $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function escape($msg) {
|
||||
$msg = str_replace("&", "&", $msg);
|
||||
$msg = str_replace("[", "[", $msg);
|
||||
$msg = str_replace("]", "]", $msg);
|
||||
public static function escape($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
public static function encode($str) {
|
||||
return self::escape($str);
|
||||
/**
|
||||
* 转义CQ码的特殊字符
|
||||
* @param $msg
|
||||
* @param false $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function encode($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息中所有的CQ码并返回移除CQ码后的消息
|
||||
* @param $msg
|
||||
* @return string
|
||||
*/
|
||||
public static function removeCQ($msg) {
|
||||
while (($cq = self::getCQ($msg)) !== null) {
|
||||
$msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg);
|
||||
$final = "";
|
||||
$last_end = 0;
|
||||
foreach(self::getAllCQ($msg) as $k => $v) {
|
||||
$final .= mb_substr($msg, $last_end, $v["start"] - $last_end);
|
||||
$last_end = $v["end"] + 1;
|
||||
}
|
||||
return $msg;
|
||||
$final .= mb_substr($msg, $last_end);
|
||||
return $final;
|
||||
}
|
||||
|
||||
public static function getCQ($msg) {
|
||||
if (($start = mb_strpos($msg, '[')) === false) return null;
|
||||
if (($end = mb_strpos($msg, ']')) === false) return null;
|
||||
$msg = mb_substr($msg, $start + 1, $end - $start - 1);
|
||||
if (mb_substr($msg, 0, 3) != "CQ:") return null;
|
||||
$msg = mb_substr($msg, 3);
|
||||
$msg2 = explode(",", $msg);
|
||||
$type = array_shift($msg2);
|
||||
$array = [];
|
||||
foreach ($msg2 as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$array[$sk] = implode("=", $ss);
|
||||
/**
|
||||
* 获取消息中第一个CQ码
|
||||
* @param $msg
|
||||
* @param bool $is_object
|
||||
* @return array|CQObject|null
|
||||
*/
|
||||
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, "]");
|
||||
if ($close === false) return null;
|
||||
$content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg));
|
||||
$exp = explode(",", $content);
|
||||
$cq["type"] = array_shift($exp);
|
||||
foreach ($exp as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
|
||||
}
|
||||
$cq["start"] = $head;
|
||||
$cq["end"] = $close + $head;
|
||||
return !$is_object ? $cq : CQObject::fromArray($cq);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return ["type" => $type, "params" => $array, "start" => $start, "end" => $end];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息中所有的CQ码
|
||||
* @param $msg
|
||||
* @param bool $is_object
|
||||
* @return array|CQObject[]
|
||||
*/
|
||||
public static function getAllCQ($msg, $is_object = false) {
|
||||
$cqs = [];
|
||||
$offset = 0;
|
||||
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
|
||||
$key_offset = mb_substr($submsg, $head);
|
||||
$tmpmsg = mb_strpos($key_offset, "]");
|
||||
if ($tmpmsg === false) break; // 没闭合,不算CQ码
|
||||
$content = mb_substr($submsg, $head + 4, $tmpmsg + $head - mb_strlen($submsg));
|
||||
$exp = explode(",", $content);
|
||||
$cq = [];
|
||||
$cq["type"] = array_shift($exp);
|
||||
foreach ($exp as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
|
||||
}
|
||||
$cq["start"] = $offset + $head;
|
||||
$cq["end"] = $offset + $tmpmsg + $head;
|
||||
$offset += $tmpmsg + 1;
|
||||
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
|
||||
}
|
||||
return $cqs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@
|
||||
|
||||
namespace ZM\API;
|
||||
|
||||
use Co;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
use ZM\Utils\CoMessage;
|
||||
|
||||
trait CQAPI
|
||||
{
|
||||
/**
|
||||
* @param ConnectionObject $connection
|
||||
* @param $connection
|
||||
* @param $reply
|
||||
* @param |null $function
|
||||
* @return bool|array
|
||||
@@ -35,21 +34,20 @@ trait CQAPI
|
||||
$r[$api_id] = [
|
||||
"data" => $reply,
|
||||
"time" => microtime(true),
|
||||
"self_id" => $connection->getOption("connect_id")
|
||||
"self_id" => $connection->getOption("connect_id"),
|
||||
"echo" => $api_id
|
||||
];
|
||||
if ($function === true) $r[$api_id]["coroutine"] = Co::getuid();
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
if (server()->push($connection->getFd(), json_encode($reply))) {
|
||||
if ($function === true) {
|
||||
Co::suspend();
|
||||
return CoMessage::yieldByWS($r[$api_id], ["echo"], 60);
|
||||
} else {
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
$data = $r[$api_id];
|
||||
unset($r[$api_id]);
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
return isset($data['result']) ? $data['result'] : null;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
@@ -77,10 +75,11 @@ trait CQAPI
|
||||
* @return bool
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function processHttpAPI($connection, $reply, $function = null) {
|
||||
public function processHttpAPI($connection, $reply, $function = null): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
public function __call($name, $arguments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\API;
|
||||
@@ -59,7 +61,7 @@ class ZMRobot
|
||||
public static function getAllRobot() {
|
||||
$r = ManagerGM::getAllByName('qq');
|
||||
$obj = [];
|
||||
foreach($r as $v) {
|
||||
foreach ($r as $v) {
|
||||
$obj[] = new ZMRobot($v);
|
||||
}
|
||||
return $obj;
|
||||
@@ -228,9 +230,9 @@ class ZMRobot
|
||||
/**
|
||||
* 群组单人禁言
|
||||
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
|
||||
* @param int $group_id
|
||||
* @param int $user_id
|
||||
* @param int $duration
|
||||
* @param $group_id
|
||||
* @param $user_id
|
||||
* @param $duration
|
||||
* @return array|bool|null
|
||||
*/
|
||||
public function setGroupBan($group_id, $user_id, $duration = 1800) {
|
||||
|
||||
@@ -12,7 +12,7 @@ abstract class AnnotationBase
|
||||
|
||||
public $class;
|
||||
|
||||
public function __toString() {
|
||||
public function __toString(): string {
|
||||
$str = __CLASS__ . ": ";
|
||||
foreach ($this as $k => $v) {
|
||||
$str .= "\n\t" . $k . " => ";
|
||||
|
||||
@@ -9,11 +9,10 @@ use ZM\Console\Console;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Http\{HandleAfter, HandleBefore, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Module\Closed;
|
||||
use ZM\Http\RouteManager;
|
||||
use ZM\Utils\DataProvider;
|
||||
|
||||
class AnnotationParser
|
||||
{
|
||||
@@ -48,7 +47,7 @@ class AnnotationParser
|
||||
*/
|
||||
public function registerMods() {
|
||||
foreach ($this->path_list as $path) {
|
||||
Console::debug("parsing annotation in ".$path[0]);
|
||||
Console::debug("parsing annotation in " . $path[0]);
|
||||
$all_class = getAllClasses($path[0], $path[1]);
|
||||
$this->reader = new AnnotationReader();
|
||||
foreach ($all_class as $v) {
|
||||
@@ -91,6 +90,7 @@ class AnnotationParser
|
||||
if ($vs instanceof ErgodicAnnotation) {
|
||||
foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) {
|
||||
$copy = clone $vs;
|
||||
/** @noinspection PhpUndefinedFieldInspection */
|
||||
$copy->method = $method->getName();
|
||||
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
|
||||
}
|
||||
@@ -125,10 +125,7 @@ class AnnotationParser
|
||||
Console::debug("解析注解完毕!");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function generateAnnotationEvents() {
|
||||
public function generateAnnotationEvents(): array {
|
||||
$o = [];
|
||||
foreach ($this->annotation_map as $module => $obj) {
|
||||
foreach (($obj["class_annotations"] ?? []) as $class_annotation) {
|
||||
@@ -151,17 +148,17 @@ class AnnotationParser
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMiddlewares() { return $this->middlewares; }
|
||||
public function getMiddlewares(): array { return $this->middlewares; }
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getMiddlewareMap() { return $this->middleware_map; }
|
||||
public function getMiddlewareMap(): array { return $this->middleware_map; }
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getReqMapping() { return $this->req_mapping; }
|
||||
public function getReqMapping(): array { return $this->req_mapping; }
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
@@ -171,7 +168,7 @@ class AnnotationParser
|
||||
|
||||
//private function below
|
||||
|
||||
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class) {
|
||||
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class): array {
|
||||
$result = [
|
||||
"class" => "\\" . $reflection_class->getName(),
|
||||
"name" => $vs->name
|
||||
|
||||
@@ -26,7 +26,7 @@ class CQAfter extends AnnotationBase implements Level
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLevel() {
|
||||
public function getLevel(): int {
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class CQBefore extends AnnotationBase implements Level
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLevel() {
|
||||
public function getLevel(): int {
|
||||
return $this->level;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
namespace ZM\Annotation\CQ;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
@@ -33,7 +32,7 @@ class CQMessage extends AnnotationBase implements Level
|
||||
/** @var int */
|
||||
public $level = 20;
|
||||
|
||||
public function getLevel() { return $this->level; }
|
||||
public function getLevel(): int { return $this->level; }
|
||||
|
||||
public function setLevel(int $level) {
|
||||
$this->level = $level;
|
||||
|
||||
@@ -27,7 +27,7 @@ class CQMetaEvent extends AnnotationBase implements Level
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLevel() { return $this->level; }
|
||||
public function getLevel(): int { return $this->level; }
|
||||
|
||||
/**
|
||||
* @param int $level
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
namespace ZM\Annotation\Http;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\Interfaces\Rule;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
|
||||
36
src/ZM/Annotation/Swoole/OnTask.php
Normal file
36
src/ZM/Annotation/Swoole/OnTask.php
Normal 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(): string {
|
||||
return $this->rule;
|
||||
}
|
||||
}
|
||||
16
src/ZM/Annotation/Swoole/OnTaskEvent.php
Normal file
16
src/ZM/Annotation/Swoole/OnTaskEvent.php
Normal 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
|
||||
{
|
||||
}
|
||||
@@ -26,12 +26,12 @@ class BuildCommand extends Command
|
||||
// ...
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$this->output = $output;
|
||||
$target_dir = $input->getOption("target") ?? (__DIR__ . '/../../../resources/');
|
||||
if (mb_strpos($target_dir, "../")) $target_dir = realpath($target_dir);
|
||||
if ($target_dir === false) {
|
||||
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (".__DIR__ . '/../../../resources/'.")" . TermColor::RESET);
|
||||
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (" . __DIR__ . '/../../../resources/' . ")" . TermColor::RESET);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$output->writeln("Target: " . $target_dir . " , Version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["version"]));
|
||||
@@ -51,7 +51,7 @@ class BuildCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function build ($target_dir, $filename) {
|
||||
private function build($target_dir, $filename) {
|
||||
@unlink($target_dir . $filename);
|
||||
$phar = new Phar($target_dir . $filename);
|
||||
$phar->startBuffering();
|
||||
|
||||
@@ -13,7 +13,7 @@ abstract class DaemonCommand extends Command
|
||||
{
|
||||
protected $daemon_file = null;
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
|
||||
if (!file_exists($pid_path)) {
|
||||
$output->writeln("<comment>没有检测到正在运行的守护进程!</comment>");
|
||||
|
||||
@@ -15,7 +15,7 @@ class DaemonReloadCommand extends DaemonCommand
|
||||
$this->setDescription("重载守护进程下的用户代码(仅限--daemon模式可用)");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
parent::execute($input, $output);
|
||||
system("kill -USR1 " . intval($this->daemon_file["pid"]));
|
||||
$output->writeln("<info>成功重载!</info>");
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace ZM\Command;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Utils\DataProvider;
|
||||
|
||||
class DaemonStatusCommand extends DaemonCommand
|
||||
{
|
||||
@@ -16,7 +15,7 @@ class DaemonStatusCommand extends DaemonCommand
|
||||
$this->setDescription("查看守护进程框架的运行状态(仅限--daemon模式可用)");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
parent::execute($input, $output);
|
||||
$output->writeln("<info>框架运行中,pid:" . $this->daemon_file["pid"] . "</info>");
|
||||
$output->writeln("<comment>----- 以下是stdout内容 -----</comment>");
|
||||
|
||||
@@ -16,10 +16,10 @@ class DaemonStopCommand extends DaemonCommand
|
||||
$this->setDescription("停止守护进程下运行的框架(仅限--daemon模式可用)");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
parent::execute($input, $output);
|
||||
system("kill -TERM ".intval($this->daemon_file["pid"]));
|
||||
unlink(DataProvider::getWorkingDir()."/.daemon_pid");
|
||||
system("kill -TERM " . intval($this->daemon_file["pid"]));
|
||||
unlink(DataProvider::getWorkingDir() . "/.daemon_pid");
|
||||
$output->writeln("<info>成功停止!</info>");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class InitCommand extends Command
|
||||
// ...
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
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;
|
||||
@@ -96,7 +96,7 @@ class InitCommand extends Command
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
private function getExtractFiles() {
|
||||
private function getExtractFiles(): array {
|
||||
return $this->extract_files;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace ZM\Command;
|
||||
|
||||
|
||||
use Swoole\Atomic;
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\Http\Response;
|
||||
use Swoole\Http\Server;
|
||||
@@ -17,7 +16,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
|
||||
@@ -34,23 +35,32 @@ class PureHttpCommand extends Command
|
||||
// ...
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
$tty_width = explode(" ", trim(exec("stty size")))[1];
|
||||
if(realpath($input->getArgument('dir') ?? '.') === false) {
|
||||
$output->writeln("<error>Directory error(".($input->getArgument('dir') ?? '.')."): no such file or directory.</error>");
|
||||
if (realpath($input->getArgument('dir') ?? '.') === false) {
|
||||
$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,28 +70,22 @@ 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();
|
||||
if($num != 0)
|
||||
echo "[$i]: ".$num."\n";
|
||||
if ($num != 0)
|
||||
echo "[$i]: " . $num . "\n";
|
||||
}
|
||||
$server->shutdown();
|
||||
$server->stop();
|
||||
});
|
||||
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))
|
||||
|
||||
@@ -22,24 +22,24 @@ 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 | 启动框架");
|
||||
$this->setHelp("直接运行可以启动");
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
if (($opt = $input->getOption("env")) !== null) {
|
||||
if (!in_array($opt, ["production", "staging", "development", ""])) {
|
||||
$output->writeln("<error> \"--env\" option only accept production, development, staging and [empty] ! </error>");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
if (LOAD_MODE == 0) echo "* This is repository mode.\n";
|
||||
(new Framework($input->getOptions()))->start();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class SystemdCommand extends Command
|
||||
// the name of the command (the part after "bin/console")
|
||||
protected static $defaultName = 'systemd:generate';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int {
|
||||
//TODO: 写一个生成systemd配置的功能,给2.0
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -18,31 +18,33 @@ use ZM\Utils\DataProvider;
|
||||
|
||||
class ConsoleApplication extends Application
|
||||
{
|
||||
const VERSION_ID = 388;
|
||||
const VERSION = "2.3.1";
|
||||
|
||||
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() {
|
||||
public function initEnv(): 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__ . "/../../"));
|
||||
elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL;
|
||||
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
|
||||
}
|
||||
if (LOAD_MODE == 2) {
|
||||
// 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";
|
||||
if (LOAD_MODE == 0) {
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
|
||||
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
|
||||
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载,是否添加?[Y/n] ";
|
||||
@@ -52,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 "添加失败!请按任意键继续!";
|
||||
@@ -88,7 +90,7 @@ class ConsoleApplication extends Application
|
||||
* @param OutputInterface|null $output
|
||||
* @return int
|
||||
*/
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null) {
|
||||
public function run(InputInterface $input = null, OutputInterface $output = null): int {
|
||||
try {
|
||||
return parent::run($input, $output);
|
||||
} catch (Exception $e) {
|
||||
@@ -96,15 +98,10 @@ class ConsoleApplication extends Application
|
||||
}
|
||||
}
|
||||
|
||||
private function selfCheck() {
|
||||
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19");
|
||||
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 (!extension_loaded("gd")) die("Can not find gd extension.\n");
|
||||
//if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
|
||||
if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n");
|
||||
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
|
||||
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
|
||||
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
|
||||
if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ use Co;
|
||||
use Exception;
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\WebSocket\Frame;
|
||||
use swoole_server;
|
||||
use Swoole\WebSocket\Server;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Exception\WaitTimeoutException;
|
||||
use ZM\Http\Response;
|
||||
@@ -26,19 +28,19 @@ class Context implements ContextInterface
|
||||
public function __construct($cid) { $this->cid = $cid; }
|
||||
|
||||
/**
|
||||
* @return swoole_server|null
|
||||
* @return Server
|
||||
*/
|
||||
public function getServer() { return self::$context[$this->cid]["server"] ?? server(); }
|
||||
public function getServer(): ?Server { return self::$context[$this->cid]["server"] ?? server(); }
|
||||
|
||||
/**
|
||||
* @return Frame|null
|
||||
*/
|
||||
public function getFrame() { return self::$context[$this->cid]["frame"] ?? null; }
|
||||
public function getFrame(): ?Frame { return self::$context[$this->cid]["frame"] ?? null; }
|
||||
|
||||
public function getFd() { return self::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; }
|
||||
public function getFd(): ?int { return self::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; }
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
* @return mixed
|
||||
*/
|
||||
public function getData() { return self::$context[$this->cid]["data"] ?? null; }
|
||||
|
||||
@@ -47,25 +49,25 @@ class Context implements ContextInterface
|
||||
/**
|
||||
* @return Request|null
|
||||
*/
|
||||
public function getRequest() { return self::$context[$this->cid]["request"] ?? null; }
|
||||
public function getRequest(): ?Request { return self::$context[$this->cid]["request"] ?? null; }
|
||||
|
||||
/**
|
||||
* @return Response|null
|
||||
*/
|
||||
public function getResponse() { return self::$context[$this->cid]["response"] ?? null; }
|
||||
public function getResponse(): ?Response { return self::$context[$this->cid]["response"] ?? null; }
|
||||
|
||||
/** @return ConnectionObject|null */
|
||||
/** @return ConnectionObject|null|Response */
|
||||
public function getConnection() { return ManagerGM::get($this->getFd()); }
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getCid() { return $this->cid; }
|
||||
public function getCid(): ?int { return $this->cid; }
|
||||
|
||||
/**
|
||||
* @return ZMRobot|null
|
||||
*/
|
||||
public function getRobot() {
|
||||
public function getRobot(): ?ZMRobot {
|
||||
$conn = ManagerGM::get($this->getFrame()->fd);
|
||||
return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null;
|
||||
}
|
||||
@@ -86,7 +88,7 @@ class Context implements ContextInterface
|
||||
|
||||
public function setDiscussId($id) { self::$context[$this->cid]["data"]["discuss_id"] = $id; }
|
||||
|
||||
public function getMessageType() { return $this->getData()["message_type"] ?? null; }
|
||||
public function getMessageType(): ?string { return $this->getData()["message_type"] ?? null; }
|
||||
|
||||
public function setMessageType($type) { self::$context[$this->cid]["data"]["message_type"] = $type; }
|
||||
|
||||
@@ -103,30 +105,44 @@ class Context implements ContextInterface
|
||||
* @param $msg
|
||||
* @param bool $yield
|
||||
* @return mixed
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public function reply($msg, $yield = false) {
|
||||
switch ($this->getData()["message_type"]) {
|
||||
case "group":
|
||||
case "private":
|
||||
case "discuss":
|
||||
$this->setCache("has_reply", true);
|
||||
$data = $this->getData();
|
||||
$conn = $this->getConnection();
|
||||
switch ($data["message_type"]) {
|
||||
case "group":
|
||||
return (new ZMRobot($conn))->setCallback($yield)->sendGroupMsg($data["group_id"], $msg);
|
||||
case "private":
|
||||
return (new ZMRobot($conn))->setCallback($yield)->sendPrivateMsg($data["user_id"], $msg);
|
||||
}
|
||||
return null;
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $msg
|
||||
* @param false $yield
|
||||
* @return mixed|void
|
||||
* @throws InterruptException
|
||||
*/
|
||||
public function finalReply($msg, $yield = false) {
|
||||
self::$context[$this->cid]["cache"]["block_continue"] = true;
|
||||
if ($msg == "") return true;
|
||||
return $this->reply($msg, $yield);
|
||||
if ($msg != "") $this->reply($msg, $yield);
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +152,7 @@ class Context implements ContextInterface
|
||||
* @return string
|
||||
* @throws InvalidArgumentException
|
||||
* @throws WaitTimeoutException
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") {
|
||||
if (!isset($this->getData()["user_id"], $this->getData()["message"], $this->getData()["self_id"]))
|
||||
@@ -145,7 +162,7 @@ class Context implements ContextInterface
|
||||
if ($prompt != "") $this->reply($prompt);
|
||||
|
||||
try {
|
||||
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]);
|
||||
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())], $timeout);
|
||||
} catch (Exception $e) {
|
||||
$r = false;
|
||||
}
|
||||
@@ -250,6 +267,7 @@ class Context implements ContextInterface
|
||||
*/
|
||||
public function getNumArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_NUMBER, $prompt_msg); }
|
||||
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
public function cloneFromParent() {
|
||||
set_coroutine_params(self::$context[Co::getPcid()] ?? self::$context[$this->cid]);
|
||||
return context();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\Context;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php /** @noinspection PhpComposerExtensionStubsInspection */
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
|
||||
|
||||
namespace ZM\DB;
|
||||
@@ -31,15 +33,14 @@ class DB
|
||||
|
||||
/**
|
||||
* @param $table_name
|
||||
* @param bool $enable_cache
|
||||
* @return Table
|
||||
* @throws DbException
|
||||
*/
|
||||
public static function table($table_name) {
|
||||
public static function table($table_name): Table {
|
||||
if (Table::getTableInstance($table_name) === null) {
|
||||
if (in_array($table_name, self::$table_list))
|
||||
return new Table($table_name);
|
||||
elseif(SqlPoolStorage::$sql_pool !== null){
|
||||
elseif (SqlPoolStorage::$sql_pool !== null) {
|
||||
throw new DbException("Table " . $table_name . " not exist in database.");
|
||||
} else {
|
||||
throw new DbException("Database connection not exist or connect failed. Please check sql configuration");
|
||||
@@ -61,7 +62,7 @@ class DB
|
||||
* @return bool
|
||||
* @throws DbException
|
||||
*/
|
||||
public static function unprepared($line) {
|
||||
public static function unprepared($line): bool {
|
||||
try {
|
||||
$conn = SqlPoolStorage::$sql_pool->get();
|
||||
if ($conn === false) {
|
||||
@@ -85,7 +86,8 @@ class DB
|
||||
* @throws DbException
|
||||
*/
|
||||
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
|
||||
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
|
||||
if (!is_array($params)) $params = [$params];
|
||||
Console::debug("MySQL: " . $line . " | " . implode(", ", $params));
|
||||
try {
|
||||
$conn = SqlPoolStorage::$sql_pool->get();
|
||||
if ($conn === false) {
|
||||
@@ -95,6 +97,7 @@ class DB
|
||||
$ps = $conn->prepare($line);
|
||||
if ($ps === false) {
|
||||
SqlPoolStorage::$sql_pool->put(null);
|
||||
/** @noinspection PhpUndefinedFieldInspection */
|
||||
throw new DbException("SQL语句查询错误," . $line . ",错误信息:" . $conn->error);
|
||||
} else {
|
||||
if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) {
|
||||
@@ -115,7 +118,7 @@ class DB
|
||||
return $ps->fetchAll($fetch_mode);
|
||||
}
|
||||
} catch (DbException $e) {
|
||||
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
zm_sleep(0.2);
|
||||
Console::warning("Gone away of MySQL! retrying!");
|
||||
return self::rawQuery($line, $params);
|
||||
@@ -123,7 +126,7 @@ class DB
|
||||
Console::warning($e->getMessage());
|
||||
throw $e;
|
||||
} catch (PDOException $e) {
|
||||
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
zm_sleep(0.2);
|
||||
Console::warning("Gone away of MySQL! retrying!");
|
||||
return self::rawQuery($line, $params);
|
||||
@@ -133,7 +136,7 @@ class DB
|
||||
}
|
||||
}
|
||||
|
||||
public static function isTableExists($table) {
|
||||
public static function isTableExists($table): bool {
|
||||
return in_array($table, self::$table_list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,6 @@ class InsertBody
|
||||
* @throws DbException
|
||||
*/
|
||||
public function save() {
|
||||
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES ('.implode(',', array_fill(0, count($this->row), '?')).')', $this->row);
|
||||
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES (' . implode(',', array_fill(0, count($this->row), '?')) . ')', $this->row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class SelectBody
|
||||
/**
|
||||
* @throws DbException
|
||||
*/
|
||||
public function count() {
|
||||
public function count(): int {
|
||||
$this->select_thing = ["count(*)"];
|
||||
$str = $this->queryPrepare();
|
||||
$this->result = DB::rawQuery($str[0], $str[1]);
|
||||
@@ -81,7 +81,7 @@ class SelectBody
|
||||
|
||||
public function getResult() { return $this->result; }
|
||||
|
||||
public function equals(SelectBody $body) {
|
||||
public function equals(SelectBody $body): bool {
|
||||
if ($this->select_thing != $body->getSelectThing()) return false;
|
||||
elseif ($this->where_thing == $body->getWhereThing()) return false;
|
||||
else return true;
|
||||
@@ -95,9 +95,9 @@ class SelectBody
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getWhereThing() { return $this->where_thing; }
|
||||
public function getWhereThing(): array { return $this->where_thing; }
|
||||
|
||||
private function queryPrepare() {
|
||||
private function queryPrepare(): array {
|
||||
$msg = "SELECT " . implode(", ", $this->select_thing) . " FROM " . $this->table->getTableName();
|
||||
$sql = $this->table->paintWhereSQL($this->where_thing['='] ?? [], '=');
|
||||
if ($sql[0] != '') {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\DB;
|
||||
|
||||
|
||||
|
||||
class Table
|
||||
{
|
||||
private $table_name;
|
||||
@@ -28,7 +29,7 @@ class Table
|
||||
return new SelectBody($this, $what == [] ? ["*"] : $what);
|
||||
}
|
||||
|
||||
public function where($column, $operation_or_value, $value = null){
|
||||
public function where($column, $operation_or_value, $value = null) {
|
||||
return (new SelectBody($this, ["*"]))->where($column, $operation_or_value, $value);
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ class Table
|
||||
return new DeleteBody($this);
|
||||
}
|
||||
|
||||
public function statement($line){
|
||||
public function statement() {
|
||||
$this->cache = [];
|
||||
//TODO: 无返回的statement语句
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class UpdateBody
|
||||
* @var array
|
||||
*/
|
||||
private $set_value;
|
||||
|
||||
/**
|
||||
* UpdateBody constructor.
|
||||
* @param Table $table
|
||||
@@ -31,19 +32,19 @@ class UpdateBody
|
||||
/**
|
||||
* @throws DbException
|
||||
*/
|
||||
public function save(){
|
||||
public function save() {
|
||||
$arr = [];
|
||||
$msg = [];
|
||||
foreach($this->set_value as $k => $v) {
|
||||
$msg []= $k .' = ?';
|
||||
$arr[]=$v;
|
||||
foreach ($this->set_value as $k => $v) {
|
||||
$msg [] = $k . ' = ?';
|
||||
$arr[] = $v;
|
||||
}
|
||||
if(($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
|
||||
$line = 'UPDATE '.$this->table->getTableName().' SET '.implode(', ', $msg);
|
||||
if($this->where_thing != []) {
|
||||
if (($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
|
||||
$line = 'UPDATE ' . $this->table->getTableName() . ' SET ' . implode(', ', $msg);
|
||||
if ($this->where_thing != []) {
|
||||
list($sql, $param) = $this->getWhereSQL();
|
||||
$arr = array_merge($arr, $param);
|
||||
$line .= ' WHERE '.$sql;
|
||||
$line .= ' WHERE ' . $sql;
|
||||
}
|
||||
return DB::rawQuery($line, $arr);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\DB;
|
||||
@@ -15,17 +15,17 @@ trait WhereBody
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getWhereSQL(){
|
||||
protected function getWhereSQL() {
|
||||
$param = [];
|
||||
$msg = '';
|
||||
foreach($this->where_thing as $k => $v) {
|
||||
foreach($v as $ks => $vs) {
|
||||
if($param != []) {
|
||||
$msg .= ' AND '.$ks ." $k ?";
|
||||
foreach ($this->where_thing as $k => $v) {
|
||||
foreach ($v as $ks => $vs) {
|
||||
if ($param != []) {
|
||||
$msg .= ' AND ' . $ks . " $k ?";
|
||||
} else {
|
||||
$msg .= "$ks $k ?";
|
||||
}
|
||||
$param []=$vs;
|
||||
$param [] = $vs;
|
||||
}
|
||||
}
|
||||
if ($msg == '') $msg = 1;
|
||||
|
||||
26
src/ZM/Entity/CQObject.php
Normal file
26
src/ZM/Entity/CQObject.php
Normal 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): CQObject {
|
||||
return new CQObject($arr["type"], $arr["params"] ?? [], $arr["start"], $arr["end"]);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Event;
|
||||
@@ -9,7 +9,6 @@ use Error;
|
||||
use Exception;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
@@ -32,7 +31,7 @@ class EventDispatcher
|
||||
/** @var bool */
|
||||
private $log = false;
|
||||
/** @var int */
|
||||
private $eid = 0;
|
||||
private $eid;
|
||||
/** @var int */
|
||||
public $status = self::STATUS_NORMAL;
|
||||
/** @var mixed */
|
||||
@@ -64,22 +63,18 @@ class EventDispatcher
|
||||
|
||||
public function __construct(string $class = '') {
|
||||
$this->class = $class;
|
||||
try {
|
||||
$this->eid = ZMAtomic::get("_event_id")->add(1);
|
||||
$list = LightCacheInside::get("wait_api", "event_trace");
|
||||
} catch (ZMException $e) {
|
||||
$list = [];
|
||||
}
|
||||
$this->eid = ZMAtomic::get("_event_id")->add(1);
|
||||
$list = LightCacheInside::get("wait_api", "event_trace");
|
||||
if (isset($list[$class])) $this->log = true;
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 开始分发事件: " . $class);
|
||||
}
|
||||
|
||||
public function setRuleFunction(callable $rule = null) {
|
||||
public function setRuleFunction(callable $rule = null): EventDispatcher {
|
||||
$this->rule = $rule;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setReturnFunction(callable $return_func) {
|
||||
public function setReturnFunction(callable $return_func): EventDispatcher {
|
||||
$this->return_func = $return_func;
|
||||
return $this;
|
||||
}
|
||||
@@ -87,27 +82,24 @@ class EventDispatcher
|
||||
/**
|
||||
* @param mixed ...$params
|
||||
* @throws Exception
|
||||
* @throws InterruptException
|
||||
*/
|
||||
public function dispatchEvents(...$params) {
|
||||
try {
|
||||
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
|
||||
$this->dispatchEvent($v, $this->rule, ...$params);
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
|
||||
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
|
||||
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
|
||||
if (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
|
||||
($this->return_func)($this->store);
|
||||
}
|
||||
}
|
||||
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
|
||||
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;
|
||||
} catch (Exception $e) {
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
throw $e;
|
||||
} catch (Error $e) {
|
||||
} catch (Exception | Error $e) {
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
throw $e;
|
||||
}
|
||||
@@ -117,9 +109,10 @@ class EventDispatcher
|
||||
* @param mixed $v
|
||||
* @param null $rule_func
|
||||
* @param mixed ...$params
|
||||
* @throws AnnotationException
|
||||
* @throws InterruptException
|
||||
* @return bool
|
||||
* @throws InterruptException
|
||||
* @throws AnnotationException
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public function dispatchEvent($v, $rule_func = null, ...$params) {
|
||||
$q_c = $v->class;
|
||||
|
||||
@@ -10,7 +10,9 @@ use Swoole\Timer;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Swoole\OnTick;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\ZMAtomic;
|
||||
|
||||
class EventManager
|
||||
@@ -34,6 +36,7 @@ class EventManager
|
||||
|
||||
/**
|
||||
* 注册所有计时器给每个进程
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function registerTimerTick() {
|
||||
$dispatcher = new EventDispatcher(OnTick::class);
|
||||
@@ -59,5 +62,11 @@ class EventManager
|
||||
}
|
||||
});
|
||||
}
|
||||
$conf = ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if (server()->worker_id == $conf["worker"]) {
|
||||
zm_timer_tick(ZMConfig::get("global", "light_cache")["auto_save_interval"] * 1000, function () {
|
||||
LightCache::savePersistence();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace ZM\Event;
|
||||
|
||||
|
||||
use Closure;
|
||||
use Co;
|
||||
use Error;
|
||||
use Exception;
|
||||
@@ -16,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;
|
||||
@@ -25,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;
|
||||
@@ -41,14 +45,13 @@ use ZM\Exception\InterruptException;
|
||||
use ZM\Framework;
|
||||
use ZM\Http\Response;
|
||||
use ZM\Module\QQBot;
|
||||
use ZM\Store\LightCache;
|
||||
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
|
||||
@@ -57,12 +60,17 @@ 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) {
|
||||
$var = trim(fgets($r));
|
||||
$fget = fgets($r);
|
||||
if ($fget === false) {
|
||||
Event::del($r);
|
||||
return;
|
||||
}
|
||||
$var = trim($fget);
|
||||
try {
|
||||
Terminal::executeCommand($var, $r);
|
||||
} catch (Exception $e) {
|
||||
@@ -71,20 +79,25 @@ 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 by keyboard 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"]) {
|
||||
if (Framework::$argv["daemon"]) {
|
||||
$daemon_data = json_encode([
|
||||
"pid" => \server()->master_pid,
|
||||
"stdout" => ZMConfig::get("global")["swoole"]["log_file"]
|
||||
],128|256);
|
||||
file_put_contents(DataProvider::getWorkingDir()."/.daemon_pid", $daemon_data);
|
||||
], 128 | 256);
|
||||
file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data);
|
||||
}
|
||||
if (Framework::$argv["watch"]) {
|
||||
if (extension_loaded('inotify')) {
|
||||
@@ -114,8 +127,12 @@ class ServerEventHandler
|
||||
* @SwooleHandler("WorkerStop")
|
||||
* @param $server
|
||||
* @param $worker_id
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onWorkerStop(Server $server, $worker_id) {
|
||||
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
|
||||
LightCache::savePersistence();
|
||||
}
|
||||
Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
|
||||
}
|
||||
|
||||
@@ -144,7 +161,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运行完成前先让其他协程等待执行
|
||||
@@ -218,7 +235,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]);
|
||||
@@ -229,6 +246,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());
|
||||
@@ -238,14 +256,14 @@ class ServerEventHandler
|
||||
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
|
||||
Console::error("Maybe it caused by your own code if in your own Module directory.");
|
||||
Console::log($e->getTraceAsString(), 'gray');
|
||||
ZMUtil::stop();
|
||||
posix_kill($server->master_pid, SIGINT);
|
||||
}
|
||||
} else {
|
||||
// 这里是TaskWorker初始化的内容部分
|
||||
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());
|
||||
@@ -255,7 +273,7 @@ class ServerEventHandler
|
||||
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
|
||||
Console::error("Maybe it caused by your own code if in your own Module directory.");
|
||||
Console::log($e->getTraceAsString(), 'gray');
|
||||
ZMUtil::stop();
|
||||
posix_kill($server->master_pid, SIGINT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +319,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();
|
||||
}
|
||||
|
||||
@@ -314,7 +332,7 @@ class ServerEventHandler
|
||||
*/
|
||||
public function onRequest(?Request $request, ?\Swoole\Http\Response $response) {
|
||||
$response = new Response($response);
|
||||
foreach(ZMConfig::get("global")["http_header"] as $k => $v) {
|
||||
foreach (ZMConfig::get("global")["http_header"] as $k => $v) {
|
||||
$response->setHeader($k, $v);
|
||||
}
|
||||
unset(Context::$context[Co::getCid()]);
|
||||
@@ -401,6 +419,21 @@ class ServerEventHandler
|
||||
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
|
||||
unset(Context::$context[Co::getCid()]);
|
||||
$type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? "");
|
||||
$access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? "";
|
||||
$token = ZMConfig::get("global", "access_token");
|
||||
if ($token instanceof Closure) {
|
||||
if (!$token($access_token)) {
|
||||
$server->close($request->fd);
|
||||
Console::warning("Unauthorized access_token: " . $access_token);
|
||||
return;
|
||||
}
|
||||
} elseif (is_string($token)) {
|
||||
if ($access_token !== $token && $token !== "") {
|
||||
$server->close($request->fd);
|
||||
Console::warning("Unauthorized access_token: " . $access_token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$type_conn = ManagerGM::getTypeClassName($type);
|
||||
ManagerGM::pushConnect($request->fd, $type_conn);
|
||||
$conn = ManagerGM::get($request->fd);
|
||||
@@ -436,7 +469,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);
|
||||
@@ -483,7 +516,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);
|
||||
@@ -491,16 +524,20 @@ class ServerEventHandler
|
||||
|
||||
/**
|
||||
* @SwooleHandler("pipeMessage")
|
||||
* @param $server
|
||||
* @param Server $server
|
||||
* @param $src_worker_id
|
||||
* @param $data
|
||||
* @throws InterruptException
|
||||
* @throws Exception
|
||||
*/
|
||||
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];
|
||||
@@ -516,6 +553,11 @@ class ServerEventHandler
|
||||
$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;
|
||||
@@ -556,19 +598,53 @@ class ServerEventHandler
|
||||
* @SwooleHandler("task")
|
||||
* @param Server|null $server
|
||||
* @param Server\Task $task
|
||||
* @return mixed
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -601,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Exception;
|
||||
use ZM\Annotation\Swoole\OnSetup;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Console\TermColor;
|
||||
use ZM\Event\ServerEventHandler;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\LightCacheInside;
|
||||
@@ -44,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");
|
||||
@@ -73,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,
|
||||
@@ -85,37 +85,45 @@ 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);
|
||||
|
||||
$out = [
|
||||
"host" => ZMConfig::get("global", "host"),
|
||||
"port" => ZMConfig::get("global", "port"),
|
||||
"log_level" => Console::getLevel(),
|
||||
"version" => ZM_VERSION,
|
||||
"config" => $args["env"] === null ? 'global.php' : $args["env"]
|
||||
];
|
||||
if(APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
|
||||
// 打印初始信息
|
||||
$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["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_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) {
|
||||
$out["worker_num"] = $num;
|
||||
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
|
||||
$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["swoole_version"] = SWOOLE_VERSION;
|
||||
}
|
||||
|
||||
$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);
|
||||
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;
|
||||
Console::setServer(self::$server);
|
||||
self::printMotd($tty_width);
|
||||
|
||||
global $asd;
|
||||
$asd = get_included_files();
|
||||
// 注册 Swoole Server 的事件
|
||||
@@ -167,12 +175,29 @@ class Framework
|
||||
} catch (Exception $e) {
|
||||
Console::error("Framework初始化出现错误,请检查!");
|
||||
Console::error($e->getMessage());
|
||||
Console::debug($e);
|
||||
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() {
|
||||
self::$server->start();
|
||||
zm_atomic("server_is_stopped")->set(1);
|
||||
Console::setLevel(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,16 +248,7 @@ class Framework
|
||||
private function parseCliArgs($args) {
|
||||
$coroutine_mode = true;
|
||||
global $terminal_id;
|
||||
$terminal_id = call_user_func(function () {
|
||||
try {
|
||||
$data = random_bytes(16);
|
||||
} catch (Exception $e) {
|
||||
return "";
|
||||
}
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
|
||||
});
|
||||
$terminal_id = uuidgen();
|
||||
foreach ($args as $x => $y) {
|
||||
switch ($x) {
|
||||
case 'disable-coroutine':
|
||||
@@ -280,6 +296,7 @@ class Framework
|
||||
Console::$theme = $y;
|
||||
}
|
||||
break;
|
||||
case 'show-php-ver':
|
||||
default:
|
||||
//Console::info("Calculating ".$x);
|
||||
//dump($y);
|
||||
@@ -289,7 +306,93 @@ class Framework
|
||||
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
|
||||
}
|
||||
|
||||
public static function getTtyWidth() {
|
||||
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(): string {
|
||||
return explode(" ", trim(exec("stty size")))[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\Http;
|
||||
|
||||
|
||||
use ZM\Console\Console;
|
||||
|
||||
class Response
|
||||
{
|
||||
|
||||
@@ -162,7 +162,7 @@ class Response
|
||||
* @return mixed
|
||||
*/
|
||||
public function end($content = null) {
|
||||
if(!$this->is_end) {
|
||||
if (!$this->is_end) {
|
||||
$this->is_end = true;
|
||||
return $this->response->end($content);
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -16,7 +17,7 @@ class RouteManager
|
||||
public static $routes = null;
|
||||
|
||||
public static function importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations) {
|
||||
if(self::$routes === null) self::$routes = new RouteCollection();
|
||||
if (self::$routes === null) self::$routes = new RouteCollection();
|
||||
|
||||
// 拿到所属方法的类上面有没有控制器的注解
|
||||
$prefix = '';
|
||||
@@ -27,11 +28,33 @@ class RouteManager
|
||||
}
|
||||
}
|
||||
$tail = trim($vss->route, "/");
|
||||
$route_name = $prefix.($tail === "" ? "" : "/").$tail;
|
||||
Console::debug("添加路由:".$route_name);
|
||||
$route_name = $prefix . ($tail === "" ? "" : "/") . $tail;
|
||||
Console::debug("添加路由:" . $route_name);
|
||||
$route = new Route($route_name, ['_class' => $class, '_method' => $method]);
|
||||
$route->setMethods($vss->request_method);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,14 @@ class StaticFileHandler
|
||||
public function __construct($filename, $path) {
|
||||
$full_path = realpath($path . "/" . $filename);
|
||||
$response = ctx()->getResponse();
|
||||
Console::debug("Full path: ".$full_path);
|
||||
Console::debug("Full path: " . $full_path);
|
||||
if ($full_path !== false) {
|
||||
if (strpos($full_path, $path) !== 0) {
|
||||
$response->status(403);
|
||||
$response->end("403 Forbidden");
|
||||
return true;
|
||||
} else {
|
||||
if(is_file($full_path)) {
|
||||
if (is_file($full_path)) {
|
||||
$exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown");
|
||||
$response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream");
|
||||
$response->end(file_get_contents($full_path));
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace ZM\Module;
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use Exception;
|
||||
use ZM\Annotation\CQ\CQAPIResponse;
|
||||
use ZM\Annotation\CQ\CQBefore;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
@@ -14,8 +14,6 @@ use ZM\Annotation\CQ\CQRequest;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\InterruptException;
|
||||
use ZM\Exception\WaitTimeoutException;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Utils\CoMessage;
|
||||
|
||||
/**
|
||||
@@ -26,27 +24,27 @@ class QQBot
|
||||
{
|
||||
/**
|
||||
* @throws InterruptException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle() {
|
||||
try {
|
||||
$data = json_decode(context()->getFrame()->data, true);
|
||||
set_coroutine_params(["data" => $data]);
|
||||
if (isset($data["post_type"])) {
|
||||
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL;
|
||||
set_coroutine_params(["data" => $data]);
|
||||
ctx()->setCache("level", 0);
|
||||
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
|
||||
if ($data["post_type"] != "meta_event") {
|
||||
$r = $this->dispatchBeforeEvents($data); // before在这里执行,元事件不执行before为减少不必要的调试日志
|
||||
if ($r->store === "block") EventDispatcher::interrupt();
|
||||
}
|
||||
if (CoMessage::resumeByWS()) {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
//Console::warning("最上数据包:".json_encode($data));
|
||||
$this->dispatchEvents($data);
|
||||
} else {
|
||||
$this->dispatchAPIResponse($data);
|
||||
}
|
||||
if (isset($data["echo"]) || isset($data["post_type"])) {
|
||||
if (CoMessage::resumeByWS()) EventDispatcher::interrupt();
|
||||
}
|
||||
if (isset($data["post_type"])) $this->dispatchEvents($data);
|
||||
else $this->dispatchAPIResponse($data);
|
||||
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
|
||||
$e->module->finalReply($e->getMessage());
|
||||
}
|
||||
@@ -55,9 +53,9 @@ class QQBot
|
||||
/**
|
||||
* @param $data
|
||||
* @return EventDispatcher
|
||||
* @throws InterruptException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function dispatchBeforeEvents($data) {
|
||||
public function dispatchBeforeEvents($data): EventDispatcher {
|
||||
$before = new EventDispatcher(CQBefore::class);
|
||||
$before->setRuleFunction(function ($v) use ($data) {
|
||||
return $v->cq_event == $data["post_type"];
|
||||
@@ -72,6 +70,7 @@ class QQBot
|
||||
/**
|
||||
* @param $data
|
||||
* @throws InterruptException
|
||||
* @throws Exception
|
||||
*/
|
||||
private function dispatchEvents($data) {
|
||||
//Console::warning("最xia数据包:".json_encode($data));
|
||||
@@ -177,45 +176,16 @@ class QQBot
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $req
|
||||
* @throws Exception
|
||||
*/
|
||||
private function dispatchAPIResponse($req) {
|
||||
$status = $req["status"];
|
||||
$retcode = $req["retcode"];
|
||||
$data = $req["data"];
|
||||
if (isset($req["echo"]) && is_numeric($req["echo"])) {
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
if (isset($r[$req["echo"]])) {
|
||||
$origin = $r[$req["echo"]];
|
||||
$self_id = $origin["self_id"];
|
||||
$response = [
|
||||
"status" => $status,
|
||||
"retcode" => $retcode,
|
||||
"data" => $data,
|
||||
"self_id" => $self_id,
|
||||
"echo" => $req["echo"]
|
||||
];
|
||||
set_coroutine_params(["cq_response" => $response]);
|
||||
$dispatcher = new EventDispatcher(CQAPIResponse::class);
|
||||
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
|
||||
return $response->retcode == ctx()->getCQResponse()["retcode"];
|
||||
});
|
||||
$dispatcher->dispatchEvents($response);
|
||||
|
||||
$origin_ctx = ctx()->copy();
|
||||
set_coroutine_params($origin_ctx);
|
||||
if (($origin["coroutine"] ?? false) !== false) {
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
$r[$req["echo"]]["result"] = $response;
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
Coroutine::resume($origin['coroutine']);
|
||||
}
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
unset($r[$req["echo"]]);
|
||||
LightCacheInside::set("wait_api", "wait_api", $r);
|
||||
SpinLock::unlock("wait_api");
|
||||
}
|
||||
}
|
||||
set_coroutine_params(["cq_response" => $req]);
|
||||
$dispatcher = new EventDispatcher(CQAPIResponse::class);
|
||||
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
|
||||
return $response->retcode == ctx()->getCQResponse()["retcode"];
|
||||
});
|
||||
$dispatcher->dispatchEvents($req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ 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;
|
||||
|
||||
class LightCache
|
||||
@@ -24,6 +27,7 @@ class LightCache
|
||||
* @param $config
|
||||
* @return bool|mixed
|
||||
* @throws Exception
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public static function init($config) {
|
||||
self::$config = $config;
|
||||
@@ -84,6 +88,7 @@ class LightCache
|
||||
* @param int $expire
|
||||
* @return mixed
|
||||
* @throws ZMException
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public static function set(string $key, $value, int $expire = -1) {
|
||||
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
|
||||
@@ -117,6 +122,7 @@ class LightCache
|
||||
* @param $value
|
||||
* @return bool|mixed
|
||||
* @throws ZMException
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public static function update(string $key, $value) {
|
||||
if (self::$kv_table === null) throw new ZMException("not initialized LightCache.");
|
||||
@@ -151,7 +157,7 @@ class LightCache
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function isset(string $key) {
|
||||
public static function isset(string $key): bool {
|
||||
return self::get($key) !== null;
|
||||
}
|
||||
|
||||
@@ -159,7 +165,7 @@ class LightCache
|
||||
return self::$kv_table->del($key);
|
||||
}
|
||||
|
||||
public static function getAll() {
|
||||
public static function getAll(): array {
|
||||
$r = [];
|
||||
$del = [];
|
||||
foreach (self::$kv_table as $k => $v) {
|
||||
@@ -175,7 +181,19 @@ class LightCache
|
||||
return $r;
|
||||
}
|
||||
|
||||
public static function savePersistence() {
|
||||
/**
|
||||
* @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;
|
||||
|
||||
if (self::$kv_table === null) return;
|
||||
$r = [];
|
||||
foreach (self::$kv_table as $k => $v) {
|
||||
@@ -184,11 +202,13 @@ class LightCache
|
||||
$r[$k] = self::parseGet($v);
|
||||
}
|
||||
}
|
||||
if(self::$config["persistence_path"] == "") return;
|
||||
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\"!");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static function checkExpire($key) {
|
||||
|
||||
@@ -13,29 +13,24 @@ class LightCacheInside
|
||||
/** @var Table[]|null */
|
||||
private static $kv_table = [];
|
||||
|
||||
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 $result;
|
||||
}
|
||||
return $result;
|
||||
public static function init(): bool {
|
||||
try {
|
||||
self::createTable("wait_api", 3, 65536);
|
||||
self::createTable("connect", 3, 64); //用于存单机器人模式下的机器人fd的
|
||||
self::createTable("static_route", 64, 256);//用于存储
|
||||
} catch (ZMException $e) {
|
||||
return false;
|
||||
} //用于存协程等待的状态内容的
|
||||
//self::createTable("worker_start", 2, 1024);//用于存启动服务器时的状态的
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param string $key
|
||||
* @return mixed|null
|
||||
* @throws ZMException
|
||||
*/
|
||||
public static function get(string $table, string $key) {
|
||||
if (!isset(self::$kv_table[$table])) throw new ZMException("not initialized LightCache");
|
||||
$r = self::$kv_table[$table]->get($key);
|
||||
return $r === false ? null : json_decode($r["value"], true);
|
||||
}
|
||||
@@ -45,10 +40,8 @@ class LightCacheInside
|
||||
* @param string $key
|
||||
* @param string|array|int $value
|
||||
* @return mixed
|
||||
* @throws ZMException
|
||||
*/
|
||||
public static function set(string $table, string $key, $value) {
|
||||
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
|
||||
public static function set(string $table, string $key, $value): bool {
|
||||
try {
|
||||
return self::$kv_table[$table]->set($key, [
|
||||
"value" => json_encode($value, 256)
|
||||
@@ -61,4 +54,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("内存不足,创建静态表失败!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Store\Lock;
|
||||
@@ -15,23 +15,21 @@ class SpinLock
|
||||
|
||||
private static $delay = 1;
|
||||
|
||||
public static function init($key_cnt, $delay = 1)
|
||||
{
|
||||
public static function init($key_cnt, $delay = 1) {
|
||||
self::$kv_lock = new Table($key_cnt, 0.7);
|
||||
self::$delay = $delay;
|
||||
self::$kv_lock->column('lock_num', Table::TYPE_INT, 8);
|
||||
return self::$kv_lock->create();
|
||||
}
|
||||
|
||||
public static function lock(string $key)
|
||||
{
|
||||
public static function lock(string $key) {
|
||||
while (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) { //此资源已经被锁上了
|
||||
if(Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
|
||||
if (Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
|
||||
else usleep(self::$delay * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public static function tryLock(string $key) {
|
||||
public static function tryLock(string $key): bool {
|
||||
if (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) {
|
||||
return false;
|
||||
}
|
||||
@@ -41,4 +39,10 @@ class SpinLock
|
||||
public static function unlock(string $key) {
|
||||
return self::$kv_lock->set($key, ['lock_num' => 0]);
|
||||
}
|
||||
|
||||
public static function transaction(string $key, callable $function) {
|
||||
SpinLock::lock($key);
|
||||
$function();
|
||||
SpinLock::unlock($key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php /** @noinspection PhpComposerExtensionStubsInspection */
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
|
||||
|
||||
namespace ZM\Store\Redis;
|
||||
@@ -16,7 +18,7 @@ class ZMRedis
|
||||
* @throws NotInitializedException
|
||||
*/
|
||||
public static function call(callable $callable) {
|
||||
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
$r = ZMRedisPool::$pool->get();
|
||||
$result = $callable($r);
|
||||
if (isset($r->wasted)) ZMRedisPool::$pool->put(null);
|
||||
@@ -29,14 +31,14 @@ class ZMRedis
|
||||
* @throws NotInitializedException
|
||||
*/
|
||||
public function __construct() {
|
||||
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
$this->conn = ZMRedisPool::$pool->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redis
|
||||
*/
|
||||
public function get() {
|
||||
public function get(): Redis {
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ class ZMRedisPool
|
||||
);
|
||||
try {
|
||||
$r = self::$pool->get()->ping('123');
|
||||
if(strpos(strtolower($r), "123") !== false) {
|
||||
if (strpos(strtolower($r), "123") !== false) {
|
||||
Console::debug("成功连接redis连接池!");
|
||||
} else {
|
||||
var_dump($r);
|
||||
}
|
||||
} catch (RedisException $e) {
|
||||
Console::error("Redis init failed! ".$e->getMessage());
|
||||
Console::error("Redis init failed! " . $e->getMessage());
|
||||
self::$pool = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\Store;
|
||||
@@ -29,72 +29,68 @@ class WorkerCache
|
||||
}
|
||||
|
||||
public static function set($key, $value, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
self::$store[$key] = $value;
|
||||
return true;
|
||||
} else {
|
||||
$action = ["action" => $async ? "asyncSetWorkerCache" : "setWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
if(!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
unset(self::$transfer[zm_cid()]);
|
||||
return $p;
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
public static function hasKey($key, $subkey) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
return isset(self::$store[$key][$subkey]);
|
||||
} else {
|
||||
$action = ["hasKeyWorkerCache", "key" => $key, "subkey" => $subkey, "cid" => zm_cid()];
|
||||
return self::processRemote($action, false, $config);
|
||||
}
|
||||
}
|
||||
|
||||
private static function processRemote($action, $async, $config) {
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
if (!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
unset(self::$transfer[zm_cid()]);
|
||||
return $p;
|
||||
}
|
||||
|
||||
public static function unset($key, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
unset(self::$store[$key]);
|
||||
return true;
|
||||
} else {
|
||||
$action = ["action" => $async ? "asyncUnsetWorkerCache" : "unsetWorkerCache", "key" => $key, "cid" => zm_cid()];
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
if(!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
unset(self::$transfer[zm_cid()]);
|
||||
return $p;
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
public static function add($key, int $value, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
if(!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
if (!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
self::$store[$key] += $value;
|
||||
return true;
|
||||
} else {
|
||||
$action = ["action" => $async ? "asyncAddWorkerCache" : "addWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
// if(!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
unset(self::$transfer[zm_cid()]);
|
||||
return $p;
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
public static function sub($key, int $value, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
if(!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
if (!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
self::$store[$key] -= $value;
|
||||
return true;
|
||||
} else {
|
||||
$action = ["action" => $async ? "asyncSubWorkerCache" : "subWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
// if(!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
unset(self::$transfer[zm_cid()]);
|
||||
return $p;
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class ZMAtomic
|
||||
* @param $name
|
||||
* @return Atomic|null
|
||||
*/
|
||||
public static function get($name) {
|
||||
public static function get($name): ?Atomic {
|
||||
return self::$atomics[$name] ?? null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace ZM\Utils;
|
||||
|
||||
|
||||
use Co;
|
||||
use Exception;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
@@ -16,8 +15,8 @@ class CoMessage
|
||||
* @param array $hang
|
||||
* @param array $compare
|
||||
* @param int $timeout
|
||||
* @return bool
|
||||
* @throws Exception
|
||||
* @return mixed
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public static function yieldByWS(array $hang, array $compare, $timeout = 600) {
|
||||
$cid = Co::getuid();
|
||||
@@ -40,7 +39,7 @@ class CoMessage
|
||||
Co::suspend();
|
||||
SpinLock::lock("wait_api");
|
||||
$sess = LightCacheInside::get("wait_api", "wait_api");
|
||||
$result = $sess[$api_id]["result"];
|
||||
$result = $sess[$api_id]["result"] ?? null;
|
||||
unset($sess[$api_id]);
|
||||
LightCacheInside::set("wait_api", "wait_api", $sess);
|
||||
SpinLock::unlock("wait_api");
|
||||
@@ -48,4 +47,35 @@ class CoMessage
|
||||
if ($result === null) return false;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function resumeByWS(): bool {
|
||||
$dat = ctx()->getData();
|
||||
$last = null;
|
||||
SpinLock::lock("wait_api");
|
||||
$all = LightCacheInside::get("wait_api", "wait_api") ?? [];
|
||||
foreach ($all as $k => $v) {
|
||||
if (!isset($v["compare"])) continue;
|
||||
foreach ($v["compare"] as $vs) {
|
||||
if (!isset($v[$vs], $dat[$vs])) continue 2;
|
||||
if ($v[$vs] != $dat[$vs]) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$last = $k;
|
||||
}
|
||||
if ($last !== null) {
|
||||
$all[$last]["result"] = $dat;
|
||||
LightCacheInside::set("wait_api", "wait_api", $all);
|
||||
SpinLock::unlock("wait_api");
|
||||
if ($all[$last]["worker_id"] != server()->worker_id) {
|
||||
ZMUtil::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]);
|
||||
} else {
|
||||
Co::resume($all[$last]["coroutine"]);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
SpinLock::unlock("wait_api");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
|
||||
class DataProvider
|
||||
{
|
||||
public static $buffer_list = [];
|
||||
|
||||
public static function getResourceFolder() {
|
||||
public static function getResourceFolder(): string {
|
||||
return self::getWorkingDir() . '/resources/';
|
||||
}
|
||||
|
||||
public static function getWorkingDir() {
|
||||
if(LOAD_MODE == 0) return 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;
|
||||
@@ -28,4 +29,29 @@ class DataProvider
|
||||
public static function getDataFolder() {
|
||||
return ZM_DATA;
|
||||
}
|
||||
|
||||
public static function saveToJson($filename, $file_array) {
|
||||
$path = ZMConfig::get("global", "config_dir");
|
||||
$r = explode("/", $filename);
|
||||
if(count($r) == 2) {
|
||||
$path = $path . $r[0]."/";
|
||||
if(!is_dir($path)) mkdir($path);
|
||||
$name = $r[1];
|
||||
} elseif (count($r) != 1) {
|
||||
Console::warning("存储失败,文件名只能有一级目录");
|
||||
return false;
|
||||
} else {
|
||||
$name = $r[0];
|
||||
}
|
||||
return file_put_contents($path.$name.".json", json_encode($file_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
public static function loadFromJson($filename) {
|
||||
$path = ZMConfig::get("global", "config_dir");
|
||||
if(file_exists($path.$filename.".json")) {
|
||||
return json_decode(file_get_contents($path.$filename.".json"), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpMissingReturnTypeInspection */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
@@ -17,6 +17,7 @@ use ZM\Http\RouteManager;
|
||||
|
||||
class HttpUtil
|
||||
{
|
||||
/** @noinspection PhpMissingReturnTypeInspection */
|
||||
public static function parseUri($request, $response, $uri, &$node, &$params) {
|
||||
$context = new RequestContext();
|
||||
$context->setMethod($request->server['request_method']);
|
||||
|
||||
79
src/ZM/Utils/MessageUtil.php
Normal file
79
src/ZM/Utils/MessageUtil.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
|
||||
use ZM\API\CQ;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
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): bool {
|
||||
$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): string {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
@@ -15,6 +15,7 @@ trait SingletonTrait
|
||||
|
||||
/**
|
||||
* @return self
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
*/
|
||||
public static function getInstance() {
|
||||
if (null === self::$instance) {
|
||||
|
||||
26
src/ZM/Utils/TaskManager.php
Normal file
26
src/ZM/Utils/TaskManager.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
|
||||
use ZM\Console\Console;
|
||||
|
||||
class TaskManager
|
||||
{
|
||||
/**
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
* @param $task_name
|
||||
* @param int $timeout
|
||||
* @param mixed ...$params
|
||||
* @return false|mixed
|
||||
*/
|
||||
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"];
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
@@ -16,6 +16,8 @@ class Terminal
|
||||
* @param string $cmd
|
||||
* @param $resource
|
||||
* @return bool
|
||||
* @noinspection PhpMissingReturnTypeInspection
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public static function executeCommand(string $cmd, $resource) {
|
||||
$it = explodeMsg($cmd);
|
||||
|
||||
@@ -11,13 +11,19 @@ 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
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
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);
|
||||
@@ -30,7 +36,17 @@ class ZMUtil
|
||||
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) {
|
||||
@@ -51,4 +67,8 @@ class ZMUtil
|
||||
return ZMBuf::$instance[$class];
|
||||
}
|
||||
}
|
||||
|
||||
public static function sendActionToWorker($target_id, $action, $data) {
|
||||
server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ 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", 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"));
|
||||
@mkdir(ZM_DATA);
|
||||
@mkdir(CRASH_DIR);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php #plain
|
||||
<?php /** @noinspection PhpUnused */ #plain
|
||||
|
||||
use Swoole\Atomic;
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\WebSocket\Server;
|
||||
use ZM\API\ZMRobot;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
@@ -11,6 +13,7 @@ use ZM\Exception\RobotNotFoundException;
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Framework;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\ZMAtomic;
|
||||
use ZM\Store\ZMBuf;
|
||||
use ZM\Utils\DataProvider;
|
||||
use Swoole\Coroutine\System;
|
||||
@@ -24,6 +27,7 @@ function phar_classloader($p) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once $filepath;
|
||||
} catch (Exception $e) {
|
||||
echo "Error when finding class: " . $p . PHP_EOL;
|
||||
@@ -51,7 +55,7 @@ function getClassPath($class_name) {
|
||||
* @param bool $ban_comma
|
||||
* @return array
|
||||
*/
|
||||
function explodeMsg($msg, $ban_comma = false) {
|
||||
function explodeMsg($msg, $ban_comma = false): array {
|
||||
$msg = str_replace(" ", "\n", trim($msg));
|
||||
if (!$ban_comma) {
|
||||
//$msg = str_replace(",", "\n", $msg);
|
||||
@@ -75,10 +79,10 @@ function unicode_decode($str) {
|
||||
/**
|
||||
* 获取模块文件夹下的每个类文件的类名称
|
||||
* @param $dir
|
||||
* @param string $indoor_name
|
||||
* @param $indoor_name
|
||||
* @return array
|
||||
*/
|
||||
function getAllClasses($dir, $indoor_name) {
|
||||
function getAllClasses($dir, $indoor_name): array {
|
||||
if (!is_dir($dir)) return [];
|
||||
$list = scandir($dir);
|
||||
$classes = [];
|
||||
@@ -88,13 +92,14 @@ function getAllClasses($dir, $indoor_name) {
|
||||
//echo "At " . $indoor_name . PHP_EOL;
|
||||
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
|
||||
elseif (mb_substr($v, -4) == ".php") {
|
||||
if(substr(file_get_contents($dir.$v), 6, 6) == "#plain") continue;
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir()."/composer.json"), true);
|
||||
foreach($composer["autoload"]["files"] as $fi) {
|
||||
if(realpath(DataProvider::getWorkingDir()."/".$fi) == realpath($dir.$v)) {
|
||||
if (substr(file_get_contents($dir . $v), 6, 6) == "#plain") continue;
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
|
||||
foreach ($composer["autoload"]["files"] as $fi) {
|
||||
if (realpath(DataProvider::getWorkingDir() . "/" . $fi) == realpath($dir . $v)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
if ($v == "global_function.php") continue;
|
||||
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
|
||||
$classes [] = $class_name;
|
||||
}
|
||||
@@ -102,7 +107,7 @@ function getAllClasses($dir, $indoor_name) {
|
||||
return $classes;
|
||||
}
|
||||
|
||||
function matchPattern($pattern, $context) {
|
||||
function matchPattern($pattern, $context): bool {
|
||||
if (mb_substr($pattern, 0, 1) == "" && mb_substr($context, 0, 1) == "")
|
||||
return true;
|
||||
if ('*' == mb_substr($pattern, 0, 1) && "" != mb_substr($pattern, 1, 1) && "" == mb_substr($context, 0, 1))
|
||||
@@ -114,7 +119,7 @@ function matchPattern($pattern, $context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function split_explode($del, $str, $divide_en = false) {
|
||||
function split_explode($del, $str, $divide_en = false): array {
|
||||
$str = explode($del, $str);
|
||||
for ($i = 0; $i < mb_strlen($str[0]); $i++) {
|
||||
if (
|
||||
@@ -176,19 +181,19 @@ function matchArgs($pattern, $context) {
|
||||
} else return false;
|
||||
}
|
||||
|
||||
function connectIsQQ() {
|
||||
function connectIsQQ(): bool {
|
||||
return ctx()->getConnection()->getName() == 'qq';
|
||||
}
|
||||
|
||||
function connectIsDefault() {
|
||||
function connectIsDefault(): bool {
|
||||
return ctx()->getConnection()->getName() == 'default';
|
||||
}
|
||||
|
||||
function connectIs($type) {
|
||||
function connectIs($type): bool {
|
||||
return ctx()->getConnection()->getName() == $type;
|
||||
}
|
||||
|
||||
function getAnnotations() {
|
||||
function getAnnotations(): array {
|
||||
$s = debug_backtrace()[1];
|
||||
//echo json_encode($s, 128|256);
|
||||
$list = [];
|
||||
@@ -216,14 +221,14 @@ function set_coroutine_params($array) {
|
||||
/**
|
||||
* @return ContextInterface|null
|
||||
*/
|
||||
function context() {
|
||||
function context(): ?ContextInterface {
|
||||
return ctx();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ContextInterface|null
|
||||
*/
|
||||
function ctx() {
|
||||
function ctx(): ?ContextInterface {
|
||||
$cid = Co::getCid();
|
||||
$c_class = ZMConfig::get("global", "context_class");
|
||||
if (isset(Context::$context[$cid])) {
|
||||
@@ -240,11 +245,11 @@ function ctx() {
|
||||
|
||||
function zm_debug($msg) { Console::debug($msg); }
|
||||
|
||||
function onebot_target_id_name($message_type) {
|
||||
function onebot_target_id_name($message_type): string {
|
||||
return ($message_type == "group" ? "group_id" : "user_id");
|
||||
}
|
||||
|
||||
function zm_sleep($s = 1) {
|
||||
function zm_sleep($s = 1): bool {
|
||||
if (Coroutine::getCid() != -1) System::sleep($s);
|
||||
else usleep($s * 1000 * 1000);
|
||||
return true;
|
||||
@@ -259,23 +264,41 @@ 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) {
|
||||
go(function () use ($ms, $callable) {
|
||||
Console::debug("Adding extra timer tick of " . $ms . " ms");
|
||||
Swoole\Timer::tick($ms, $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);});
|
||||
});
|
||||
} else {
|
||||
return Swoole\Timer::tick($ms, function () use ($callable) {call_with_catch($callable);});
|
||||
}
|
||||
}
|
||||
|
||||
function zm_data_hash($v) {
|
||||
function zm_data_hash($v): string {
|
||||
return md5($v["user_id"] . "^" . $v["self_id"] . "^" . $v["message_type"] . "^" . ($v[$v["message_type"] . "_id"] ?? $v["user_id"]));
|
||||
}
|
||||
|
||||
function server() {
|
||||
function server(): ?Server {
|
||||
return Framework::$server;
|
||||
}
|
||||
|
||||
@@ -284,7 +307,7 @@ function server() {
|
||||
* @throws RobotNotFoundException
|
||||
* @throws ZMException
|
||||
*/
|
||||
function bot() {
|
||||
function bot(): ZMRobot {
|
||||
if (($conn = LightCacheInside::get("connect", "conn_fd")) == -2) {
|
||||
return ZMRobot::getRandom();
|
||||
} elseif ($conn != -1) {
|
||||
@@ -308,3 +331,26 @@ function getAllFdByConnectType(string $type = 'default'): array {
|
||||
}
|
||||
return $fds;
|
||||
}
|
||||
|
||||
function zm_atomic($name): ?Atomic {
|
||||
return ZMAtomic::get($name);
|
||||
}
|
||||
|
||||
function uuidgen($uppercase = false): string {
|
||||
try {
|
||||
$data = random_bytes(16);
|
||||
} catch (Exception $e) {
|
||||
return "";
|
||||
}
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return $uppercase ? strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))) :
|
||||
vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZMTest\Mock;
|
||||
|
||||
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\WebSocket\Frame;
|
||||
use Swoole\WebSocket\Server;
|
||||
use ZM\API\ZMRobot;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Context\ContextInterface;
|
||||
use ZM\Http\Response;
|
||||
|
||||
class Context implements ContextInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Context constructor.
|
||||
* @param $cid
|
||||
*/
|
||||
public function __construct($cid) { }
|
||||
|
||||
/**
|
||||
* @return Server
|
||||
*/
|
||||
public function getServer() {
|
||||
// TODO: Implement getServer() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Frame
|
||||
*/
|
||||
public function getFrame() {
|
||||
// TODO: Implement getFrame() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getData() {
|
||||
// TODO: Implement getData() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return mixed
|
||||
*/
|
||||
public function setData($data) {
|
||||
// TODO: Implement setData() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ConnectionObject
|
||||
*/
|
||||
public function getConnection() {
|
||||
// TODO: Implement getConnection() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|null
|
||||
*/
|
||||
public function getFd() {
|
||||
// TODO: Implement getFd() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCid() {
|
||||
// TODO: Implement getCid() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function getResponse() {
|
||||
// TODO: Implement getResponse() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest() {
|
||||
// TODO: Implement getRequest() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZMRobot
|
||||
*/
|
||||
public function getRobot() {
|
||||
// TODO: Implement getRobot() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUserId() {
|
||||
// TODO: Implement getUserId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGroupId() {
|
||||
// TODO: Implement getGroupId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDiscussId() {
|
||||
// TODO: Implement getDiscussId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getMessageType() {
|
||||
// TODO: Implement getMessageType() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRobotId() {
|
||||
// TODO: Implement getRobotId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMessage() {
|
||||
// TODO: Implement getMessage() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $msg
|
||||
* @return mixed
|
||||
*/
|
||||
public function setMessage($msg) {
|
||||
// TODO: Implement setMessage() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function setUserId($id) {
|
||||
// TODO: Implement setUserId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function setGroupId($id) {
|
||||
// TODO: Implement setGroupId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDiscussId($id) {
|
||||
// TODO: Implement setDiscussId() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $type
|
||||
* @return mixed
|
||||
*/
|
||||
public function setMessageType($type) {
|
||||
// TODO: Implement setMessageType() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCQResponse() {
|
||||
// TODO: Implement getCQResponse() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $msg
|
||||
* @param bool $yield
|
||||
* @return mixed
|
||||
*/
|
||||
public function reply($msg, $yield = false) {
|
||||
echo $msg.PHP_EOL;
|
||||
// TODO: Implement reply() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $msg
|
||||
* @param bool $yield
|
||||
* @return mixed
|
||||
*/
|
||||
public function finalReply($msg, $yield = false) {
|
||||
// TODO: Implement finalReply() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $prompt
|
||||
* @param int $timeout
|
||||
* @param string $timeout_prompt
|
||||
* @return mixed
|
||||
*/
|
||||
public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") {
|
||||
// TODO: Implement waitMessage() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $arg
|
||||
* @param $mode
|
||||
* @param $prompt_msg
|
||||
* @return mixed
|
||||
*/
|
||||
public function getArgs(&$arg, $mode, $prompt_msg) {
|
||||
$r = $arg;
|
||||
array_shift($r);
|
||||
return array_shift($r);
|
||||
// TODO: Implement getArgs() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function setCache($key, $value) {
|
||||
// TODO: Implement setCache() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCache($key) {
|
||||
// TODO: Implement getCache() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function cloneFromParent() {
|
||||
// TODO: Implement cloneFromParent() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function copy() {
|
||||
// TODO: Implement copy() method.
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user