Compare commits

...

70 Commits
1.6 ... 2.0.0

Author SHA1 Message Date
crazywhalecc
d80de2a552 initial commit for v2 2020-12-23 10:35:25 +08:00
crazywhalecc
5b85ec15e9 Merge remote-tracking branch 'origin/2.0-dev' into master
# Conflicts:
#	README.md
#	SECURITY.md
#	composer.json
#	src/Framework/FrameworkLoader.php
#	src/ZM/Event/Swoole/RequestEvent.php
#	src/ZM/Http/Response.php
2020-12-23 10:33:25 +08:00
crazywhalecc
420f326f11 update README.md 2020-12-23 10:28:20 +08:00
crazywhalecc
f361a675af update README.md 2020-12-23 10:21:48 +08:00
jerry
18c09beacb update docs 2020-12-23 01:21:51 +08:00
crazywhalecc
c15d320ef6 update docs 2020-12-22 16:46:59 +08:00
crazywhalecc
58da612121 update docs 2020-12-22 16:39:37 +08:00
crazywhalecc
64ec34d54d update docs 2020-12-22 16:35:22 +08:00
crazywhalecc
eeb952035a update docs 2020-12-22 16:31:16 +08:00
crazywhalecc
e78485273d update docs 2020-12-22 16:28:34 +08:00
crazywhalecc
9bd07e5a66 update docs 2020-12-21 16:56:36 +08:00
jerry
4deb814ff2 fix a little bug 2020-12-21 01:43:40 +08:00
jerry
619baf1691 update to 2.0.0-b9 version
fix a little bug
2020-12-20 19:15:28 +08:00
jerry
bc0bb9b6b0 update to 2.0.0-b8 version
fix environment conflict and remove custom commands
2020-12-20 18:49:03 +08:00
jerry
42d9c97711 update to 2.0.0-b7 version
fix environment getter
2020-12-20 18:40:26 +08:00
jerry
81365173d2 update to 2.0.0-b6 version
add interrupt return_value to InterruptException
2020-12-20 18:08:34 +08:00
jerry
ba5b793db7 update to 2.0.0-b5 version
set modules config to array
add subdir index.html
update Example of Hello.php
add Exception tester for TimerMiddleware.php
add keyword for @CQCommand
rename OnWorkerStart.php to OnStart.php
remove SwooleEventAfter.php
rename HandleEvent.php to SwooleHandler.php
set ZMRobot callback mode default to true
add getNextArg() and getFullArg()
add EventDispatcher.php logger
set Exception all based from ZMException
fix recursive bug for Response.php
add single_bot_mode
add SingletonTrait.php
add bot() function
2020-12-14 01:24:34 +08:00
crazywhalecc
1ffb30a471 update to v2.0.0-b4 version
change global.php config load time and logic
set context get server function available more time
delete unused comment and @CQAPISend
@CQCommand add start_with and end_with
set exceptions extended by ZMException
rename @SwooleSetup to @ZMSetup
fix quotes for global.php
fix LightCache empty presistence_path error
remove RemoteShell
2020-12-10 16:37:04 +08:00
crazywhalecc
cc31a1654d update to v1.6.5 version
correct version name
update dependencies
2020-12-09 14:06:58 +08:00
crazywhalecc
dfca486b64 update to v1.6.5 version
correct version name
2020-12-09 13:57:51 +08:00
crazywhalecc
754c2846fe update to v1.6.5 version
fix composer.json bug
2020-12-09 13:53:16 +08:00
crazywhalecc
eed670cb50 update to v1.6.4 version
fix LOAD_MODE = 1 autoload bug
remove unnecessary dependencies
2020-12-09 13:43:25 +08:00
Whale
9e824d960f Update README.md 2020-12-08 02:07:08 +08:00
Whale
992b6020a5 Update README.md 2020-12-08 02:06:48 +08:00
Whale
d51dbef437 Update README.md 2020-11-28 09:32:11 +08:00
jerry
944a9e849b update documents 2020-11-23 01:47:37 +08:00
jerry
09a11821b2 update documents 2020-11-23 01:09:57 +08:00
jerry
dbfe2c9c17 update documents 2020-11-23 00:24:33 +08:00
Whale
7f058638bd Update README.md 2020-11-23 00:07:17 +08:00
jerry
63e0594199 update README.md 2020-11-22 19:26:35 +08:00
jerry
4a4bc697d6 update docs 2020-11-22 19:21:29 +08:00
jerry
8edc3f337b update to 2.0.0-b3 2020-11-22 19:18:23 +08:00
jerry
c04130fed1 update composer file 2020-11-15 18:52:52 +08:00
jerry
7fefcb850a update to 1.6.3 version
fix response redirect bug
fix document_index not working
2020-11-15 18:50:37 +08:00
Whale
1fe54d4b94 Update README.md 2020-11-10 23:25:44 +08:00
jerry
c460b37d14 add mkdocs documents 2020-11-10 23:16:55 +08:00
jerry
7fe405d0af initial 2.0.0-b2 commit 2020-11-08 20:14:08 +08:00
jerry
3b90bf6245 initial 2.0.0-b1 commit 2020-11-08 19:40:16 +08:00
jerry
deab5fd921 initial 2.0.0-a5 commit
fix waitMessage function
fix CQCommand regexMatch and fullMatch
it just works
2020-11-04 18:43:50 +08:00
jerry
29fa9d8662 initial 2.0.0-a4 commit 2020-11-03 21:02:24 +08:00
jerry
da584e0542 initial 2.0.0-a3 commit 2020-10-03 23:00:18 +08:00
Whale
13a32bec79 Update README.md 2020-10-03 09:29:17 +08:00
jerry
f91d24aaaa initial 2.0.0-a2 commit 2020-09-29 15:07:43 +08:00
Whale
0f5786c8c4 Update LICENSE 2020-09-25 15:35:34 +08:00
Whale
4ed046769f Update LICENSE 2020-09-25 15:34:48 +08:00
Whale
690980f72d Update README.md 2020-09-25 15:33:39 +08:00
Whale
f025eeb34a Update README.md 2020-09-20 15:57:21 +08:00
Whale
d642f50ef1 Update README.md 2020-09-20 00:39:20 +08:00
Whale
2900754307 Update README.md 2020-09-08 09:50:30 +08:00
Whale
dffeac668d Update SECURITY.md 2020-09-01 17:46:03 +08:00
jerry
1510e2f0d0 initial 2.0 commit 2020-08-31 10:15:25 +08:00
jerry
82896ee4a1 initial 2.0 commit 2020-08-31 10:14:48 +08:00
jerry
beb1f5f063 initial 2.0 commit 2020-08-31 10:11:06 +08:00
Whale
b6756179f5 Update README.md 2020-08-23 23:54:24 +08:00
jerry
1adcf76203 change logo 2020-08-23 23:53:45 +08:00
Whale
5b003ab575 Add files via upload 2020-08-23 23:52:07 +08:00
Whale
75f6aa531e Update README.md 2020-08-23 23:50:11 +08:00
Whale
6e1f4820f8 Update README.md 2020-08-23 23:49:16 +08:00
Whale
50ce81334b Update README.md 2020-08-23 23:49:05 +08:00
Whale
0ed0aa089a Update README.md 2020-08-23 23:48:47 +08:00
Whale
102ba769ec Update README.md 2020-08-23 23:48:05 +08:00
Whale
e062f484b1 Update README.md 2020-08-23 23:45:45 +08:00
Whale
3be3e8412a Update README.md 2020-08-23 23:44:15 +08:00
Whale
ab5abf1c00 Update README.md 2020-08-19 15:55:51 +08:00
Whale
3aaa72cfb9 Update README.md 2020-08-14 11:06:04 +08:00
Whale
10f846c214 Update README.md 2020-08-05 09:09:03 +08:00
Whale
67a42c4be9 Update README.md 2020-08-03 21:40:54 +08:00
Whale
7e4e58a322 Update README.md 2020-08-03 21:40:21 +08:00
jerry
5de283d30c fix issue #15 at version 1.6.2 2020-07-27 09:52:52 +08:00
jerry
7513fd1a1d add downloadFile option for version 1.6.1 2020-07-26 13:43:52 +08:00
154 changed files with 6284 additions and 5443 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ zm.json
/zm_data/
composer.lock
/resources/server.phar
/distribute/
/bin/.phpunit.result.cache
/resources/zhamao.service
.phpunit.result.cache

View File

@@ -1,29 +1,3 @@
FROM ubuntu:18.04
WORKDIR /app/
RUN echo "Asia/Shanghai" > /etc/timezone
ENV LANG C.UTF_8
ENV LC_ALL C.UTF-8
ENV LANGUAGE C.UTF-8
FROM zmbot/swoole:latest
RUN apt-get update && apt-get install -y software-properties-common tzdata
RUN dpkg-reconfigure -f noninteractive tzdata
VOLUME ["/app/zhamao-framework/"]
RUN add-apt-repository ppa:ondrej/php && \
apt-get update && \
apt-get install php php-dev php-mbstring gcc make openssl \
php-mbstring php-json php-curl php-mysql -y && \
apt-get install wget composer -y && \
wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \
tar -zxvf v4.5.0.tar.gz && \
cd swoole-src-4.5.0/ && \
phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \
(echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
ADD . /app/zhamao-framework
ADD . /app/zhamao-framework-bak
#RUN cd /app/zhamao-framework && composer update && composer clearcache
#RUN mv zhamao-framework-master zhamao-framework
WORKDIR /app/zhamao-framework
CMD ["/bin/bash", "-i", "/app/zhamao-framework-bak/.entry.sh"]
# TODO: auto-setup entrypoint

View File

@@ -1,73 +1,71 @@
# zhamao-framework
<div align="center">
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
<h2>炸毛框架</h2>
炸毛框架 (zhamao-frameowork) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
协程高性能的 **QQ 机器人 + Web 服务器** 开发框架(炸毛框架)。
</div>
<img src="https://avatars0.githubusercontent.com/u/48620312" height = "200" alt="炸毛框架" align=center/>
## 开发者注意
**开发者 QQ 群670821194**
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
**2.0 版本如果有问题请第一时间加群反馈!**
## 简介
zhamao-framework 是一个基于 酷Q 的 PHP Swoole 的机器人框架,它会对 QQ 机器人收到的消息进行解析处理,并以模块化的形式进行开发,来完成机器人的自然语言对话等功能。
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架对接 酷Q 的桥梁是 **CQHTTP** 插件,这里是它的[项目地址](https://github.com/richardchien/coolq-http-api/)
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块
除了起到解析消息的作用,炸毛框架 还提供了完整的 WebSocket + HTTP 服务器,你还能用此框架构建出高性能的 API 接口服务器。
```php
/**
* @CQCommand("你好")
*/
public function hello() {
ctx()->reply("你好,我是炸毛!"); // 简单的命令式回复
}
/**
* @RequestMapping("/index")
*/
public function index() {
return "<h1>hello!</h1>"; // 快速的 HTTP 服务开发
}
```
## 开始
先安装环境,环境安装见下方文档
1. `composer create-project zhamao/framework-starter` 从模板新建基础文档结构进行使用
2. 你也可以直接到 **Release** 中下载最新的 phar 包,放入文件夹后 `php server.phar` 快速启动框架
3. 还可以使用 Dockerfile 构建 Docker 容器
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行
## 文档
Pages托管[https://framework.zhamao.xin/](https://framework.zhamao.xin/)
## 文档v2 版本)
查看文档[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/)
国内服务器[https://framework2.zhamao.xin/](https://framework2.zhamao.xin/)
备用链接[http://docs-v2.zhamao.me/](http://docs-v2.zhamao.me/)
自行构建文档:`mkdocs build -d distribute`
## 特点
- 支持多账号
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
- 灵活的注解事件绑定机制
- 支持下断点调试Psysh
- 易用的上下文,模块内随处可用
- 采用模块化编写,功能之间高内聚低耦合
- 采用模块化编写,可单独拆装功能
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL 查询器、数据库连接池等数据库连接方案
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
- 支持 phar 一键打包
## 炸毛特色模块
## 从 v1 升级
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
| 模块名称 | 说明 | 模块地址 |
| ------------------ | -------------------------------- | ------------------------------------------------------------ |
| 微信公众号兼容模块 | 为框架提供微信公众号订阅号兼容层 | [zhamao-wechat-patch](https://github.com/zhamao-robot/zhamao-wechat-patch) |
| 通用模块 | 图片上传和下载模块 | [zhamao-general-tools](https://github.com/zhamao-robot/zhamao-general-tools) |
## 计划开发内容
- [X] WebSocket测试脚本客户端
- [X] Session 和中间层管理模块
- [X] 常驻服务脚本
- [X] 一些常用的通用 API 例如经济(用户积分、亲密度等)的模块
- [ ] 图灵机器人/腾讯AI 聊天模块
- [ ] 分词模块(可能会放弃计划,因为目前好用的分词都是其他语言的)
- [ ] HTTP 过滤器、Auth 模块、完整的 MVC 兼容(可能会放弃计划,因为框架主打机器人开发)
- [ ] Redis 连接池或开箱即用的相应功能内置
- [X] 1.3 版本使用上下文代替
- [X] 更好的 Logger稳定和漂亮的控制台输出
- [ ] 日志服务
- [X] 框架支持 Phar 打包(可能会比较靠后支持)
- [ ] 完整的单元测试(如果有需求则尽快开发)
- [X] 静态文件服务器
## 从 cqbot-swoole 升级
目前新的框架采用了全新的注解机制,所以旧版的框架上写的模块到新框架需要重新编写。当然为了减少工作量,新的框架也最大限度地保留了旧版框架编写的风格,一般情况下根据新版框架的文档仅需修改少量地方即可完成重写。
旧版框架并入了 `old` 分支,如果想继续使用旧版框架请移步分支。升级过程中如果遇到问题可以找作者。
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护耗费精力较大所以非常欢迎对框架的贡献。
@@ -79,11 +77,17 @@ Pages托管[https://framework.zhamao.xin/](https://framework.zhamao.xin/)
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人3276124472是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 Issue 进行疑难解答。
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
项目框架采用 Apache-2.0 协议开源,在分发或重写修改等操作时需遵守协议。项目模块部分(`Module` 文件夹) 在非借鉴框架内代码时可不遵守 Apache-2.0 协议进行分发和修改(声明版权)。
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

View File

@@ -4,8 +4,9 @@
| Version | Supported |
| ------- | ------------------ |
| 1.2.x | :white_check_mark: |
| 1.1.x | :x: |
| 2.0 | :white_check_mark: |
| 1.6.x | :white_check_mark: |
| 1.1.x | :x: |
| 1.0.x | :x: |
## Reporting a Vulnerability

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env php
<?php /** @since 1.2.1 */
global $version;
echo "version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../composer.json"), true)["version"]) . PHP_EOL;
switch ($argv[1] ?? '') {
case '--normal':
case '':
build();
break;
case '--help':
case '-h':
default:
echo "\nzhamao-framework Phar builder.\n";
echo "\nUsage: " . $argv[0] . " [OPTION]";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n --with-wechat-patch\tReplace ModBase with wechat patch version and build your own phar package";
echo "\n --normal\t\tBuild your own phar package as normal options\n\n";
break;
}
function build($with_wechat_patch = false) {
if (ini_get('phar.readonly') == 1) {
die("You need to set \"phar.readonly\" to \"Off\"!\nSee: https://stackoverflow.com/questions/34667606/cant-enable-phar-writing\n");
}
$filename = "server.phar";
@unlink(__DIR__ . '/../resources/' . $filename);
$phar = new Phar(__DIR__ . '/../resources/' . $filename);
$phar->startBuffering();
$src = realpath(__DIR__ . '/../');
$hello = file_get_contents($src . '/src/Module/Example/Hello.php');
$middleware = file_get_contents($src . '/src/Module/Middleware/TimerMiddleware.php');
unlink($src . '/src/Module/Example/Hello.php');
unlink($src . '/src/Module/Middleware/TimerMiddleware.php');
if ($with_wechat_patch) {
global $wechat_patch;
$wechat = base64_decode($wechat_patch);
} else {
$wechat = false;
}
if ($wechat !== false) {
echo "Using wechat patch.\n";
$modbase = file_get_contents($src . '/src/ZM/ModBase.php');
unlink($src . '/src/ZM/ModBase.php');
}
$phar->buildFromDirectory($src);
$phar->addFromString('tmp/Hello.php.bak', $hello);
$phar->addFromString('tmp/TimerMiddleware.php.bak', $middleware);
if ($wechat !== false) {
$phar->addFromString('src/ZM/ModBase.php', $wechat);
file_put_contents($src . '/src/ZM/ModBase.php', $modbase);
}
//$phar->compressFiles(Phar::GZ);
$phar->setStub($phar->createDefaultStub('phar-starter.php'));
$phar->stopBuffering();
file_put_contents($src . '/src/Module/Example/Hello.php', $hello);
file_put_contents($src . '/src/Module/Middleware/TimerMiddleware.php', $middleware);
echo "Successfully built. Location: " . $src . "/resources/$filename\n";
}

65
bin/phpunit-swoole Executable file
View File

@@ -0,0 +1,65 @@
#!/usr/bin/env php
<?php
/**
* Copyright: Swlib
* Author: Twosee <twose@qq.com>
* Date: 2018/4/14 下午10:58
*/
Co::set([
'log_level' => SWOOLE_LOG_INFO,
'trace_flags' => 0
]);
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
foreach ([
__DIR__ . '/../../../autoload.php',
__DIR__ . '/../../autoload.php',
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/vendor/autoload.php'
] as $file
) {
if (file_exists($file)) {
define('PHPUNIT_COMPOSER_INSTALL', $file);
break;
}
}
if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
fwrite(
STDERR,
'You need to set up the project dependencies using Composer:' . PHP_EOL . PHP_EOL .
' composer install' . PHP_EOL . PHP_EOL .
'You can learn all about Composer on https://getcomposer.org/.' . PHP_EOL
);
die(1);
} else {
if (array_reverse(explode('/', __DIR__))[0] ?? '' === 'test') {
$vendor_dir = dirname(PHPUNIT_COMPOSER_INSTALL);
$bin_unit = "{$vendor_dir}/bin/phpunit";
$unit_uint = "{$vendor_dir}/phpunit/phpunit/phpunit";
if (file_exists($bin_unit)) {
@unlink($bin_unit);
@symlink(__FILE__, $bin_unit);
}
if (file_exists($unit_uint)) {
@unlink($unit_uint);
@symlink(__FILE__, $unit_uint);
}
}
}
require PHPUNIT_COMPOSER_INSTALL;
$starttime = microtime(true);
go(function (){
try{
PHPUnit\TextUI\Command::main(false);
} catch(Exception $e) {
echo $e->getMessage().PHP_EOL;
}
});
Swoole\Event::wait();
echo "Took ".round(microtime(true) - $starttime, 4). "s\n";

145
bin/start
View File

@@ -1,135 +1,28 @@
#!/usr/bin/env php
<?php
use Framework\FrameworkLoader;
use Scheduler\Scheduler;
use ZM\ConsoleApplication;
require __DIR__ . '/../src/Framework/FrameworkLoader.php';
require __DIR__ . '/../src/Scheduler/Scheduler.php';
// 这行是用于开发者自己电脑的调试功能
Swoole\Coroutine::set([
'max_coroutine' => 30000,
]);
global $vendor_mode;
$vendor_mode = false;
if (mb_strpos(__DIR__, getcwd()) !== false && substr(str_replace(getcwd(), "", __DIR__), 0, 8) == "/vendor/") {
$symbol = sha1(is_file("/flag2") ? file_get_contents("/flag2") : '1') == '6252c0ec7fcbd544c3d6f5f0a162f60407d7a896' || mb_strpos(getcwd(), "/private/tmp");
// 首先得判断是直接从library模式启动的框架还是从composer引入library启动的框架
// 判断方法:判断当前目录上面有没有 /vendor 目录,如果没有 /vendor 目录说明是从 composer 引入的
// 否则就是直接从 framework 项目启动的
if (!is_dir(__DIR__ . '/../vendor') || $symbol) {
define("LOAD_MODE", 1); //composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
/** @noinspection PhpIncludeInspection */
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
} elseif (substr(__DIR__, 0, 7) == 'phar://') {
define("LOAD_MODE", 2); //phar模式
// 会废弃phar启动的方式在2.0
} else {
define("LOAD_MODE", 0); //正常模式
define("LOAD_MODE", 0);
require_once __DIR__ . "/../vendor/autoload.php";
}
date_default_timezone_set("Asia/Shanghai");
switch ($argv[1] ?? '') {
case 'scheduler':
case 'timer':
go(function () {
try {
new Scheduler(Scheduler::REMOTE);
} catch (Exception $e) {
die($e->getMessage());
}
});
break;
case 'phar-build':
array_shift($argv);
require_once 'phar-build';
break;
case 'systemd':
array_shift($argv);
require_once 'systemd';
break;
case 'init':
array_shift($argv);
if (LOAD_MODE != 1) {
echo "initialization must be started with composer-project mode!\n";
exit(1);
}
$cwd = LOAD_MODE_COMPOSER_PATH;
echo "Copying default module file ...";
@mkdir($cwd . "/config");
@mkdir($cwd . "/src");
@mkdir($cwd . "/src/Custom");
@mkdir($cwd . "/src/Module");
@mkdir($cwd . "/src/Module/Example");
@mkdir($cwd . "/src/Module/Middleware");
$ls = [
"/config/global.php",
"/.gitignore",
"/config/file_header.json",
"/config/motd.txt",
"/src/Module/Example/Hello.php",
"/src/Module/Middleware/TimerMiddleware.php",
"/src/Custom/global_function.php"
];
foreach($ls as $v) {
if(!file_exists($cwd.$v)) {
echo "Copying ".$v.PHP_EOL;
copy($cwd."/vendor/zhamao/framework".$v, $cwd.$v);
}
}
$autoload = [
"psr-4" => [
"Module\\" => "src/Module",
"Custom\\" => "src/Custom"
],
"files" => [
"src/Custom/global_function.php"
]
];
$scripts = [
"server" => "vendor/bin/start server",
"server:log-debug" => "vendor/bin/start server --log-debug",
"server:log-verbose" => "vendor/bin/start server --log-verbose",
"server:log-info" => "vendor/bin/start server --log-info",
"server:log-warning" => "vendor/bin/start server --log-warning",
"server:debug-mode" => "vendor/bin/start server --debug-mode",
"systemd" => "vendor/bin/start systemd"
];
echo PHP_EOL;
if (file_exists($cwd . "/composer.json")) {
echo "Updating composer.json ...";
$composer = json_decode(file_get_contents($cwd . "/composer.json"), true);
if (!isset($composer["autoload"])) {
$composer["autoload"] = $autoload;
}
if (!isset($composer["scripts"])) {
$composer["scripts"] = $scripts;
}
file_put_contents($cwd . "/composer.json", json_encode($composer, 64 | 128 | 256));
echo PHP_EOL;
} else {
echo("Error occurred. Please check your updates.\n");
exit(1);
}
echo "success!\n";
break;
case '':
case 'framework':
case 'server':
if (!is_dir(__DIR__ . '/../vendor/') && LOAD_MODE == 0) {
echo "Warning: you have not update composer!\n";
exec("composer update", $out, $var);
if ($var != 0) {
echo "You need to run \"composer update\" at root of zhamao-framework!\n";
die;
}
}
$loader = new FrameworkLoader($argv);
break;
case '--help':
case '-h':
echo "\nUsage: " . $argv[0] . " [OPTION]\n";
echo "\nzhamao-framework start script, provides several startup arguments.";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n framework, server\tstart main framework, this is default option";
echo "\n phar-build\t\tbuild a new phar archive";
echo "\n init\t\t\tinitialize framework structure in this directory";
echo "\n systemd\t\tgenerate a new systemd \".service\" file to use\n\n";
break;
default:
echo "Unknown option \"{$argv[1]}\"!\n\"--help\" for more information\n";
break;
}
// 终端的命令行功能启动!!
$application = new ConsoleApplication("zhamao-framework");
$application->initEnv();
$application->run();

View File

@@ -3,7 +3,8 @@
"description": "High performance QQ robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "1.6",
"version": "2.0.0",
"extra": {},
"authors": [
{
"name": "whale",
@@ -16,26 +17,46 @@
],
"prefer-stable": true,
"bin": [
"bin/start"
"bin/start",
"bin/phpunit-swoole"
],
"require": {
"php": ">=7.2",
"swoole/ide-helper": "@dev",
"ext-mbstring": "*",
"swlib/saber": "^1.0",
"doctrine/annotations": "~1.10",
"ext-json": "*",
"ext-posix": "*",
"psy/psysh": "@stable",
"symfony/polyfill-ctype": "^1.20",
"symfony/polyfill-mbstring": "^1.20",
"symfony/console": "^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"
},
"suggest": {
"ext-ctype": "*",
"ext-pdo": "*",
"psy/psysh": "@stable"
"ext-mbstring": "*"
},
"autoload": {
"psr-4": {
"Custom\\": "src/Custom",
"Framework\\": "src/Framework",
"ZM\\": "src/ZM",
"Module\\": "src/Module"
}
"ZM\\": "src/ZM"
},
"files": [
"src/ZM/global_functions.php"
]
},
"autoload-dev": {
"psr-4": {
"ZMTest\\": "test/ZMTest"
},
"files": [
"test/ZMTest/Mock/mock.php"
]
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"swoole/ide-helper": "@dev"
}
}

29
config/console_color.json Normal file
View File

@@ -0,0 +1,29 @@
{
"default": {
"success": "green",
"info": "lightblue",
"warning": "yellow",
"error": "red",
"verbose": "blue",
"debug": "gray",
"trace": "gray"
},
"white-term": {
"success": "green",
"info": "",
"warning": "yellow",
"error": "red",
"verbose": "blue",
"debug": "gray",
"trace": "gray"
},
"no-color": {
"success": "",
"info": "",
"warning": "",
"error": "",
"verbose": "",
"debug": "",
"trace": ""
}
}

View File

@@ -1,4 +1,6 @@
<?php
/** @noinspection PhpFullyQualifiedNameUsageInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
global $config;
/** bind host */
@@ -8,29 +10,39 @@ $config['host'] = '0.0.0.0';
$config['port'] = 20001;
/** 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = "http://127.0.0.1:".$config['port'];
$config['http_reverse_link'] = "http://127.0.0.1:" . $config['port'];
/** 框架是否启动debug模式 */
$config['debug_mode'] = false;
/** 存放框架内文件数据的目录 */
$config['zm_data'] = realpath(__DIR__ . "/../").'/zm_data/';
$config['zm_data'] = realpath(__DIR__ . "/../") . '/zm_data/';
/** 存放各个模块配置文件的目录 */
$config['config_dir'] = $config['zm_data'].'config/';
$config['config_dir'] = $config['zm_data'] . 'config/';
/** 存放崩溃和运行日志的目录 */
$config['crash_dir'] = $config['zm_data'].'crash/';
$config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'].'swoole_error.log',
'worker_num' => 1,
'dispatch_mode' => 2,
//'task_worker_num' => 1,
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'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,
//'task_enable_coroutine' => true
];
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['sql_config'] = [
'sql_host' => '',
@@ -38,18 +50,25 @@ $config['sql_config'] = [
'sql_username' => 'name',
'sql_database' => 'db_name',
'sql_password' => '',
'sql_enable_cache' => true,
'sql_reset_cache' => '0300',
'sql_options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false
],
'sql_no_exception' => false,
'sql_default_fetch_mode' => PDO::FETCH_BOTH // added in 1.5.6
'sql_default_fetch_mode' => PDO::FETCH_ASSOC // added in 1.5.6
];
/** CQHTTP连接约定的token */
$config["access_token"] = "";
/** Redis连接信息host留空则启动时不创建Redis连接池 */
$config['redis_config'] = [
'host' => '',
'port' => 6379,
'timeout' => 1,
'db_index' => 0,
'auth' => ''
];
/** onebot连接约定的token */
$config["access_token"] = '';
/** HTTP服务器固定请求头的返回 */
$config['http_header'] = [
@@ -64,15 +83,11 @@ $config['http_default_code_page'] = [
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
'in_count' => 0, //消息接收message的统计数量
'out_count' => 0, //消息发送调用send_*_msg的统计数量
'reload_time' => 0, //调用reload功能统计数量
'wait_msg_id' => 0, //协程挂起id自增
'info_level' => 2, //终端显示的log等级
//'custom_atomic_name' => 0, //自定义添加的Atomic
];
/** 自动保存的缓存保存时间(秒 */
$config['auto_save_interval'] = 900;
/** 终端日志显示等级0-4 */
$config["info_level"] = 2;
/** 上下文接口类 implemented from ContextInterface */
$config['context_class'] = \ZM\Context\Context::class;
@@ -88,7 +103,15 @@ $config['static_file_server'] = [
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\Framework\ServerEventHandler::class, //默认不可删除,否则会不能使用框架
\ZM\Event\ServerEventHandler::class,
];
/** 服务器启用的外部第三方和内部插件 */
$config['modules'] = [
'onebot' => [
'status' => true,
'single_bot_mode' => false
], // QQ机器人事件解析器如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭
];
return $config;

View File

@@ -1,18 +0,0 @@
FROM richardchien/cqhttp:latest
RUN apt-get update && apt-get install -y software-properties-common tzdata
RUN dpkg-reconfigure -f noninteractive tzdata
RUN add-apt-repository ppa:ondrej/php && \
apt-get update && \
apt-get install php php-dev php-mbstring gcc make openssl \
php-mbstring php-json php-curl php-mysql -y && \
apt-get install wget composer -y && \
wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \
tar -zxvf v4.5.0.tar.gz && \
cd swoole-src-4.5.0/ && \
phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \
(echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
ADD start.sh /home/user/start.sh
RUN chown user:user /home/user/start.sh && chmod +x /home/user/start.sh
ADD https://github.com/zhamao-robot/zhamao-framework/archive/master.zip /home/user/master.zip
RUN chown user:user /home/user/master.zip && chmod 777 /home/user/master.zip
VOLUME ["/home/user/coolq","/home/user/zhamao-framework"]

View File

@@ -1,6 +0,0 @@
#!/bin/bash
unzip master.zip
mv zhamao-framework-master/* zhamao-framework/
cd zhamao-framework
php bin/start

1
docs/FAQ.md Normal file
View File

@@ -0,0 +1 @@
# FAQ

3
docs/advanced/index.md Normal file
View File

@@ -0,0 +1,3 @@
# 进阶开发
## 深入
还没填坑,敬请期待!

39
docs/advanced/to-v2.md Normal file
View File

@@ -0,0 +1,39 @@
# 从炸毛框架 V1 升级
> 这里只写明可能在升级过程中会影响原先代码执行的部分,不包含新增的特性等。
### 需要改变命名空间的类
- `Framework\Console` -> `ZM\Console\Console`
- `Swlib\Util\SingletonTrait` -> `ZM\Utils\SingletonTrait`
- `ZM\Annotation\Http\Before` -> `ZM\Annotation\Http\HandleBefore`
- `ZM\Annotation\Http\After` -> `ZM\Annotation\Http\HandleAfter`
- `@SwooleEventAt` -> `@OnSwooleEvent`
- 删除 `@SwooleEventAfter`
- 删除 `ModBase`
- `@HandleEvent` -> `@SwooleHandler`
- `ZM\Utils\ZMRobot` -> `\ZM\API\ZMRobot`
### 方法名称变更
- `ZM\Console::stackTrace()` -> `ZM\Console::trace()`
### 注解的变化
`@OnSwooleEvent`(原 `@SwooleEventAt`)中,`rule` 参数不再是自定义语法的东西了(比如之前的 `connectType:qq` 之类的鸡肋语法),直接是可执行的 PHP 代码,比如 `3 == 4``connectIsQQ()` 之类的。
去除 `@CQAPISend`,因为目前没什么意义。
`@CQCommand` 中,`regexMatch` 变成 `pattern``fullMatch` 变成 `regex`,消除歧义(第一个是 * 号匹配符进行匹配的,第二个是标准的正则表达式匹配)。同时新增 `start_with``end_with``keyword` 平行选项。
`@OnTick` 注解新增第二个参数 `worker_id`,其中默认是 0代表只在 `#0` 号工作进程上运行计时器。
### 中间件编写的改变
原先的 Middleware 是需要含有 `getName()` 方法才合法,现在不需要了,但是对 `@MiddlewareClass` 注解需要增加参数,也就是说原先 `getName()` 返回的名称现在需要写到 `@MiddlewareClass("xxx")` 这样的形式。
### ZMBuf 的变化
由于 2.0 框架使用了多进程模型所以不能使用原先适用于单进程下全局变量的方式ZMBuf进行存取变量所以 ZMBuf 下的所有方法都需要更改,其中 `get, set` 等对缓存操作的模型请根据 2.0 的文档变更使用 `Redis` 或内置的多进程共享内存可用的 `LightCache` 轻量缓存。
而获取全局配置文件,如 `global.php` 文件,也发生了变化,新框架引入了 `ZMConfig` 对象,可以快速地区分各类环境变量从而读取不同的配置文件。比如我们获取原先的 global 配置文件中的一项:`ZMBuf::globals("port")`,在 2.0 中需要使用 `ZMConfig::get("global", "port")` 方式。以此类推,`ZMBuf::config("xxx")` 也直接变为 `ZMConfig::get("xxx")` 了。

88
docs/assets/css/extra.css Normal file
View File

@@ -0,0 +1,88 @@
.md-header-nav__button.md-logo {
padding: .2rem;
margin: .2rem;
}
.md-header-nav__button.md-logo img, .md-header-nav__button.md-logo svg {
width: 1.6rem;
height: 1.6rem;
}
.doc-chat-container {
border-radius: 6px;
width: 100%;
min-height: 30px;
/*noinspection CssUnresolvedCustomProperty*/
background-color: var(--md-code-bg-color);
padding: 12px;
margin-right: auto;
margin-left: auto;
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
}
.doc-chat-row {
margin: 0;
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
justify-content: flex-end;
}
.doc-chat-row-robot {
justify-content: flex-start !important;
}
.doc-chat-box {
color: #000000de;
position: relative;
width: fit-content;
max-width: 55%;
border-radius: .5rem;
padding: .4rem .6rem;
margin: .4rem .8rem;
background-color: #fff;
line-height: 1.5;
font-size: 16px;
outline: none;
overflow-wrap: break-word;
white-space: normal;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.doc-chat-box:after {
content: "";
position: absolute;
right: auto;
top: 0;
width: 8px;
height: 12px;
color: #fff;
border: 0 solid transparent;
border-bottom: 7px solid;
border-radius: 0 0 8px 0;
left: calc(100% - 4px);
box-sizing: inherit;
}
.doc-chat-box-robot:after {
content: "";
position: absolute;
right: calc(100% - 4px);
top: 0;
width: 8px;
height: 12px;
color: #fff;
border: 0 solid transparent;
border-bottom: 7px solid;
border-radius: 0 0 0 8px;
left: auto;
box-sizing: inherit;
}
.doc-chat-avatar {
background-color: aquamarine;
width: 36px !important;
height: 36px !important;
border-radius: 18px;
}

BIN
docs/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/assets/logos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

3
docs/component/index.md Normal file
View File

@@ -0,0 +1,3 @@
# 框架组件
还没写到这里,不着急

56
docs/event/index.md Normal file
View File

@@ -0,0 +1,56 @@
# 事件和注解
## 注解事件概念
我们知道事件,是一个底层的 event loop 收到消息后调用对应的各类方法的一个模型,比如给机器人发送消息后框架要做的就是指定到一个你定义的函数上,处理你的业务逻辑代码。比如在默认模块中,提供了 **你好** 的回复:**你好啊,我是由炸毛框架构建的机器人!**。这项简单回复的任务就是一个事件的触发到响应的全过程。
**注解**Annotation又称标注Java 最早在 2004 年的 JDK 5 中引入的一种注释机制。目前 PHP 官方版本并未提供内置元注解和注解概念,但我们通过 `ReflectionClass` 反射类解析 PHP 代码注释从而实现了自己的一套注解机制。如果你没有写过 Java并且不了解注解是什么你可以理解为对 function 或 class 的一个修饰,因为传统的 PHP 代码逻辑我们都知道,不能简单给原先存在的函数贴标签,就比如,你不能在原本的 PHP 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。
在常见框架如 SpringSwoft 等代码结构里面,注解更是其核心的存在。
在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 Swoole 自身的框架启动事件、WebSocket 连接握手事件、HTTP 请求事件等等,也包括 CQHTTP 发来的事件,如`message``notice``request` 等。
## 如何使用注解
就像我们日常开发写注释一样,只需在类、方法或成员变量上方按规则添加注释即可,这里以默认自带的 `Hello` 模块类为例子:
```php
<?php
namespace Module\Example;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand(match="你好")
* @return string
*/
public function hello(){
return "你好啊,我是由炸毛框架构建的机器人!";
}
}
```
其中 `@CQCommand()` 就是一个基本的注解应用。注意需引入相关注解Annotation**且必须** 以 `/**` 开始并以 `*/` 结束,否则会导致无法解析!上方 `@return` 为 IDE 自动生成的 PHPDoc不需要管。
有什么用?大有妙用!这个例子内注解类的用途是收到 QQ 消息后如果消息第一个词匹配到 `你好` 的话,框架就会自动处理,最终执行调用此 `hello()` 方法。注意 `CQCommand` 和其他任何后面讲到的注解类一样,需先 `use ZM\Annotation\` 下的对应注解类,否则也不能正常使用。
### 基本语法
先 use先 use先 use重要的事情说三遍`use ZM\Annotation\xxxx;`
**必须**`/**` 开始并以 `*/` 结束。
```
@注解类名(参数名1="参数1的值"[,参数名2="参数2的值"])
```
对于只使用或只有一个参数的注解类,`@注解类名("参数的值")` 可以省略参数名。
对于没有参数的注解类,`@参数名()` 直接使用即可。
## 注解和事件的关系
在炸毛框架里,注解常常被当作事件分发的一个重要角色,但注解本身又不是事件,更恰当的说,是注解代表了事件。
机器人开发过程中常见的 `@CQCommand`,或者是 HTTP 服务器路由绑定 `@RequestMapping` 都是相当于由对应注解代表了事件,而 `@Middleware``@Closed` 等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。

View File

@@ -0,0 +1,23 @@
# OneBot 实例
## 什么是 OneBot
OneBot 是一个聊天机器人应用接口标准,详情戳[这里](https://github.com/howmanybots/onebot)。
## OneBot 实现选择
如果你使用炸毛框架作为聊天机器人的开发框架,请先选择一种兼容 OneBot 标准的机器人接口。理论上,基于 OneBot 标准开发的**任何** SDK、框架和机器人应用都可以无缝地在下面的不同实现中切换。当然在一小部分细节上各实现可能有一些不同。
| 项目地址 | 平台 | 核心作者 | 备注 |
| ------------------------------------------------------------ | --------------------------------------------- | -------------- | ------------------------------------------------------------ |
| [richardchien/coolq-http-api](https://github.com/richardchien/coolq-http-api) | CKYU | richardchien | 可在 Mirai 平台使用 [mirai-native](https://github.com/iTXTech/mirai-native) 加载 |
| [Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) | [MiraiGo](https://github.com/Mrs4s/MiraiGo) | Mrs4s | 炸毛框架推荐使用此项目机器人应用 |
| [yyuueexxiinngg/cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai) | [Mirai](https://github.com/mamoe/mirai) | yyuueexxiinngg | |
| [takayama-lily/onebot](https://github.com/takayama-lily/onebot) | [OICQ](https://github.com/takayama-lily/oicq) | takayama | |
| [ProtobufBot](https://github.com/ProtobufBot) | [Mirai](https://github.com/mamoe/mirai) | lz1998 | 事件和 API 数据内容和 OneBot 一致,通信方式不兼容 |
!!! warning "注意"
因为目前炸毛框架 2.0 只支持 WebSocket 方式的 OneBot 实现,所以目前上述项目的连接方式均只可选支持反向 WebSocket 通信的。后期会兼容 HTTP 和正向 WebSocket 通信方式。
如果你还没有自己的 QQ或者是其他原因导致的暂时无法使用上述 OneBot 实例,可以使用炸毛项目中的 OneBot 协议聊天模拟器。但目前还处在开发中,暂不可用。

142
docs/guide/基本配置.md Normal file
View File

@@ -0,0 +1,142 @@
# 基本配置
到目前为止,炸毛框架的配置文件还没有任何变更,是默认的行为。在本章内容中,将列举出炸毛框架的配置文件的规则和使用。
!!! error "警告"
因为炸毛框架的全局配置中含有数据库名称和密码以及 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` |
| `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]` |
### 子表 **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 |
### 子表 **light_cache**
| 配置名称 | 说明 | 默认值 |
| -------------------------- | ----------------------------------------------- | ---------------------------- |
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 1024 |
| `max_strlen` | 作为 value 字符串的最大长度 | 16384 |
| `hash_conflict_proportion` | Hash冲突率越大越好但是需要的内存更多 | 0.6 |
| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` |
| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 |
### 子表 **sql_config**
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------ | ------------------------------------------------------------ |
| `sql_host` | 数据库地址(留空则不使用数据库) | 空 |
| `sql_port` | 数据库端口 | 3306 |
| `sql_username` | 连接数据库的用户名 | |
| `sql_database` | 要连接的数据库名 | |
| `sql_password` | 数据库连接密码 | |
| `sql_options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_EMULATE_PREPARES => false]` |
| `sql_default_fetch_mode` | PDO 的 fetch 模式 | `PDO::FETCH_ASSOC` |
### 子表 **redis_config**
| 配置名称 | 说明 | 默认值 |
| ---------- | ------------------------------------------ | ------ |
| `host` | Redis 服务器地址,留空则启动时不创建连接池 | 空 |
| `port` | Redis 服务器端口 | 6379 |
| `timeout` | Redis 超时时间 | 1 |
| `db_index` | Redis 要连接的数据库 index | 0 |
| `auth` | 认证字符串 | 空 |
### 子表 static_file_server
| 配置名称 | 说明 | 默认值 |
| ---------------- | ---------------------- | ------------------------------ |
| `status` | 是否开启静态文件服务器 | false |
| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` |
| `document_index` | 默认索引的文件名列表 | `["index.html"]` |
## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境production / development / staging来编写配置文件。
### 使用环境参数
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `production``staging``development`
```bash
vendor/bin/start server --env=development
```
### 不同环境配置文件
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制或改名为 `global.development.php``global.production.php` 即可。
### 优先级
如果指定了 `--env` 环境参数:`global.{对应环境}.php` > `global.php`,如果两个配置文件都找不到则报错。
如果未指定 `--env` 环境参数:`global.php` > `global.development.php` > `global.staging.php` > `global.production.php`
## 其他自定义配置文件
炸毛框架的全局配置文件为 `global.php`,为了让不同的开发者更好的二次开发或者集成更多功能,炸毛框架的配置文件模块也支持自己编写的其他 `*.php``*.json` 格式的配置文件。例如炸毛框架默认附带了 `file_header.json` 这个配置文件(用来返回各类文件扩展名对应的 `Content-Type` 头参数的表)。
使用也非常简单,我们先以 `.json` 格式为例,我们创建一个 `example_a.json` 文件在 `config/` 目录(和 `global.php` 一个文件夹下),并编写自己的任意配置内容:
```json
{
"key1": "value1"
}
```
在框架中,启动后就会默认加载,使用只需要用以下方式即可:
```php
use ZM\Config\ZMConfig; # 先 use 再使用!
$r = ZMConfig::get("example_a", "key1"); # $r == "value1"
```
如果需要用到变量或其他动态的内容,可以使用 `.php` 格式的配置文件。这里还是以 `example_a.php` 来举例:
```php
<?php
$config['key1'] = "value1";
$config['starttime'] = time();
return $config;
```
使用方式同上:
```php
$r = ZMConfig::get("example_a", "key1"); # $r == "value1"
$time = ZMConfig::get("example_a", "starttime"); # $time == 服务器启动时间
```
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json``example_a.production.php` 均可。

134
docs/guide/安装.md Normal file
View File

@@ -0,0 +1,134 @@
# 安装
> 这篇为炸毛框架以及环境的部署教程。
框架部署分为环境部署和框架部署。框架部署非常简单,只需要通用的指令,下方主要说环境部署。
## Docker 部署 PHP 环境
如果你不想干扰主机的环境,可以使用 Docker 进行拉取框架适用的 PHP7 with Swoole Extension Docker Container。本框架安装教程中使用的 DockerHub 及 Dockerfile 构建文件所构建的容器均为独立的容器,和框架无关,此 Docker 也可以用作运行**其他基于 php-cli 模式的项目**。
方法一、直接拉取远程容器(推荐)
```bash
docker pull zmbot/swoole
```
方法二、从 Dockerfile 构建容器
```bash
git clone https://github.com/zhamao-robot/zhamao-swoole-docker.git
cd zhamao-swoole-docker/
docker build -t zm .
```
!!! note "从 Dockerfile 构建容器的提示"
使用 Dockerfile 构建后,需要将下方所有的 `zmbot/swoole` 全部更换成 `zm`,或者你上方指令中的 `-t` 参数后方的名称,具体可以详情查阅 Docker 的文档。
## 主机部署 PHP 环境
### Debian 系列Ubuntu、Kali
需要的系统内软件包为:`php php-dev php-mbstring gcc make openssl php-mbstring php-json php-curl php-mysql wget composer`
下面是一个一键安装的命令行(最小安装,需 root 权限):
```bash
apt-get update && apt-get install -y software-properties-common && add-apt-repository ppa:ondrej/php && apt-get update && apt-get install php php-dev php-mbstring gcc make openssl php-mbstring php-json php-curl php-mysql -y && apt-get install wget composer -y && wget https://github.com/swoole/swoole-src/archive/v4.5.7.tar.gz && tar -zxvf v4.5.7.tar.gz && cd swoole-src-4.5.7/ && phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && (echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
```
### macOS (with Homebrew)
macOS 系统下的部署相对简单很多,只需要使用 Homebrew 安装以下包和执行安装命令即可
!!! note "给 macOS 开发者的提示"
因为苹果新的 Apple Sillicon 对 Homebrew 的支持目前仅限于 Rosetta2 转译版,
所以在使用 M1-based Mac 时出现问题暂时无解。
使用以下指令可能会遇到报错等问题,如有疑问可直接使用 Docker 或咨询我(炸毛框架开发者)。
```bash
brew install php composer
pecl install swoole
```
### 其他 Linux 发行版
其他 Linux 发行版,如 CentOSFedoraArch 等暂时还没有经过严格的测试需要哪些依赖,大体和 Ubuntu、Debian 系需要的依赖包差不多,可根据安装过程中报错提示依次安装,或者直接使用 Docker 环境。
## 安装框架
恭喜你,前方通过 Docker 或主机安装环境后可以开始构建框架的开发脚手架了!
如果你是通过**主机安装 PHP 部署的环境**,下方是通过脚手架来构建项目的命令行。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
composer update
```
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole composer update
```
或者在 Docker 环境下,你可以直接使用如下方法拉取和快速启动一个最标准的框架。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter
./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出
```
## 启动框架
本地环境启动方式:
```bash
cd zhamao-framework-starter
vendor/bin/start server
```
使用 Docker 启动:
```bash
cd zhamao-framework-starter
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start server
```
启动后你会看到和下方类似的初始化内容,表明启动成功了
```verilog
$ vendor/bin/start server
host: 0.0.0.0 | port: 20001
log_level: 2 | version: 2.0.0
config: global.php | worker_num: 4
working_dir: /Users/jerry/project/git-project/zhamao-framework
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
[14:27:31] [I] [#0] Worker #0
[14:27:31] [I] [#2] Worker #2
[14:27:31] [I] [#1] Worker #1
[14:27:31] [I] [#3] Worker #3
[14:27:31] [S] [#3] Worker #3
[14:27:31] [S] [#0] Worker #0
[14:27:31] [S] [#2] Worker #2
[14:27:31] [S] [#1] Worker #1
```
单纯运行 炸毛框架 后,如果不部署或安装启动任何机器人客户端的话,仅仅相当于启动了一个 监听 20001 端口的WebSoket + HTTP 服务器。你可以通过浏览器访问http://127.0.0.1:20001 ,或者你部署到了服务器后需要输入服务器地址。
!!! note "安装和部署总结"
根据上方描述,此文档中剩余提到的所有 Bash 命令,如果使用 Docker 部署环境,则需要加上 Docker 环境的指令:`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole`,如执行其他 Linux 指令(以查看 PHP 版本为例):`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole php -v`
## 使用 IDE 等工具开发代码
我们使用文本编辑器进行炸毛框架开发,在使用集成开发环境 **IDEA****PhpStorm** 时,推荐通过插件市场搜索并安装 **PHP Annotations** 插件以提供注解命名空间自动补全、注解属性代码提醒、注解类跳转等,非常有助于提升开发效率的功能。
## 进阶环境部署和开发
炸毛框架还支持更多种启动方式,如源码模式、守护进程模式,具体后续有关环境和部署的进阶教程,请查看 [进阶开发](/advanced/) 部分!

View File

@@ -0,0 +1,4 @@
# 快速上手 - HTTP 服务器篇
HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~

View File

@@ -0,0 +1,228 @@
# 快速上手 - 机器人篇
## 简介
看到这里,你已经完成了前面的环境部署,到了最关键的第一步了!
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架的。
## 机器人客户端
开发之前请注意,**机器人客户端和框架相互独立!**故有关**机器人客户端**出现的问题请到对应机器人客户端开发者或 GitHub 项目中咨询和讨论,炸毛框架为对接机器人客户端的一个快速开发的框架。
机器人客户端是炸毛框架以外的程序或软件,目前炸毛框架支持的机器人客户端通信标准为 OneBot 标准(原 CQHTTP只要你的机器人客户端是 OneBot 标准的,就可以和炸毛框架进行无缝对接。
OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)。
这里以炸毛框架开发过程中使用的 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 来举例进行第一个机器人的配置工作。
简要步骤描述为:
1. 下载 go-cqhttp 对应平台的 [release 文件](https://github.com/Mrs4s/go-cqhttp/releases)
2. 双击 exe 文件或者使用 `./go-cqhttp` 启动
3. 生成默认配置文件并修改默认配置
!!! warning "注意"
由于 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"
{
// QQ号
uin: 你的机器人QQ
// QQ密码
password: "你的QQ密码"
// 是否启用密码加密
encrypt_password: false
// 加密后的密码, 如未启用密码加密将为空, 请勿随意修改.
password_encrypted: ""
// 是否启用内置数据库
// 启用将会增加10-20MB的内存占用和一定的磁盘空间
// 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable_db: true
// 访问密钥, 强烈推荐在公网的服务器设置
access_token: ""
// 重连设置
relogin: {
// 是否启用自动重连
// 如不启用掉线后将不会自动重连
enabled: true
// 重连延迟, 单位秒
relogin_delay: 3
// 最大重连次数, 0为无限制
max_relogin_times: 0
}
// API限速设置
// 该设置为全局生效
// 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
// 目前该限速设置为令牌桶算法, 请参考:
//https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
_rate_limit: {
// 是否启用限速
enabled: false
// 令牌回复频率, 单位秒
frequency: 1
// 令牌桶大小
bucket_size: 1
}
// 是否忽略无效的CQ码
// 如果为假将原样发送
ignore_invalid_cqcode: false
// 是否强制分片发送消息
// 分片发送将会带来更快的速度
// 但是兼容性会有些问题
force_fragmented: false
// 心跳频率, 单位秒
// -1 为关闭心跳
heartbeat_interval: 0
// HTTP设置
http_config: {
// 是否启用正向HTTP服务器
enabled: true
// 服务端监听地址
host: 0.0.0.0
// 服务端监听端口
port: 5700
// 反向HTTP超时时间, 单位秒
// 最小值为5小于5将会忽略本项设置
timeout: 0
// 反向HTTP POST地址列表
// 格式:
// {
// 地址: secret
// }
post_urls: {}
}
// 正向WS设置
ws_config: {
// 是否启用正向WS服务器
enabled: true
// 正向WS服务器监听地址
host: 0.0.0.0
// 正向WS服务器监听端口
port: 6700
}
// 反向WS设置
ws_reverse_servers: [
// 可以添加多个反向WS推送
{
// 是否启用该推送
enabled: true
// 反向WS Universal 地址
// 注意 设置了此项地址后下面两项将会被忽略
reverse_url: ws://127.0.0.1:20001/
// 反向WS API 地址
reverse_api_url: ""
// 反向WS Event 地址
reverse_event_url: ""
// 重连间隔 单位毫秒
reverse_reconnect_interval: 3000
}
]
// 上报数据类型
// 可选: string array
post_message_format: string
// 是否使用服务器下发的新地址进行重连
// 注意, 此设置可能导致在海外服务器上连接情况更差
use_sso_address: false
// 是否启用 DEBUG
debug: false
// 日志等级
// WebUi 设置
web_ui: {
// 是否启用 WebUi
enabled: true
// 监听地址
host: 127.0.0.1
// 监听端口
web_ui_port: 9999
// 是否接收来自web的输入
web_input: false
}
}
```
其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT
## 第一次对话
一旦新的配置文件正确生效之后,所在的控制台(如果正在运行的话)应该会输出类似下面的内容:
```verilog
[15:26:34] [I] [#2] 机器人 你的QQ号 已连接!
```
表明机器人已成功连接到炸毛框架了!
这时,如果你是根据安装教程走下来并且未编写任何模块,炸毛自带一个示例模块,里面含有命令:`你好``随机数`。如果你对机器人回复:`你好`,它会回复你 `你好啊,我是由炸毛框架构建的机器人!`。这一历史性的对话标志着你已经成功地运行了炸毛框架,开始了编写更强大的 QQ 机器人的创意之旅!
## 编写一个命令
让我们转到框架的模块源代码部分,目录是 `src/Module/Example`,文件是 `Hello.php`。我们插入一段这样的代码:
```php
/**
* @CQCommand("echo")
*/
public function repeat() {
$repeat = ctx()->getFullArg("请输入你要回复的内容");
ctx()->reply($repeat);
//return $repeat; // 这样的效果等同于 ctx()->reply()
}
```
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。

View File

@@ -0,0 +1,66 @@
# 注册事件响应(机器人篇)
现在模块已经创建完毕,我们可以开始编写实际代码了。本段以机器人会话为例子来讲述事件注册和响应,有关 HTTP 服务器等注册事件响应请看后面事件和注解章节。
## 机器人聊天事件处理
首先知道QQ 等聊天机器人的消息我们的处理逻辑为如下简单的模式:
- QQ 用户消息 -> 机器人客户端 -> 连接客户端的框架(炸毛框架)
- 框架处理逻辑后返回给用户的消息 -> 机器人客户端 -> QQ用户
第一步,我们以框架这边的角度考虑,我们称之为“事件”,我们编写代码所做的就是要响应这一事件。
首先我们以一句简单的功能——查天气,我们要从零实现一个查天气的功能进行示范如何快捷有效地开发一个功能。
### 确定问法
我们首先要确定的用户问法是一般由我们自己定义,但最好贴合用户的自然语言来进行定义。比如我们这里提到的天气功能,用户一般就会询问“北京天气”,“北京天气怎么样”,“天气 北京”。
### 注册消息事件
我们以最简单的命令方式“天气 北京”进行处理。问法为参数化的,通过空格来分开,这也是炸毛框架默认支持最基本的聊天事件之一。我们通过上一部分的方式新建一个单文件模块 `Weather.php``src/Module` 目录下,并编写:
```php
<?php
namespace Module;
use ZM\Annotation\CQ\CQCommand;
class Weather {
/**
* @CQCommand("天气")
* @return string
*/
public function searchWeather() {
$city = ctx()->getNextArg("请告诉我你要查询的城市"); // 发送 “天气 北京”时,变量为“北京”
// 这里假设是天气API接口的对接返回了天气的数据
$weather = "2020年12月22日-2~9℃ blablabla";
return "$city 天气情况:".$weather;
}
}
```
!!! note "提示"
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API本教程的进阶一栏后期会详细编写如何对接一个天气 API 接口。
在上方代码编写完毕后,运行框架(或运行过程中终端输入 `reload`),然后使用机器人客户端连接到炸毛框架,即可实现我们的第一个功能。我们在代码中编写了一个**注解事件**`@CQCommand`,此注解事件是用于接收用户的普通消息并切分成各类命令规则的一个注解事件绑定。代码中的注解事件省去了注解中的键名,也可以写作 `@CQCommand(match="天气")`
这里注解事件的概念请看 [事件和注解](/event/) 一栏描述的概念即可。到这里,我们就完成一个可以处理命令 `天气 xxx` 的方法了!
### 处理消息事件
第一行 `ctx()` 是炸毛框架内的上下文获取方式,每条用户聊天信息发过来,被炸毛框架收到,都会创建一次上下文,同时这次聊天的全部信息,比如用户的 IDQQ 号码),发消息的时间,如果是群消息的话所在的群号等等,都被存到了上下文中。`ctx()` 获取的是一个上下文对象,内部有许多可操作上下文的方法,其中代码的 `getNextArg()`,作用是根据空格分隔获取命令中的下一个参数。
在之前我们知道:`天气 北京` 是我们发送的消息,我们要获取到用户发送的参数 `北京``getNextArg()` 是框架封装好的一个快速获取下一个参数的方法,我们这里直接使用它来获取。
对于 `getNextArg()` 中的文本,可为空,不为空的时候如果用户只发送天气两个字,机器人还是会响应,但是它会询问你这句话,然后你回复机器人“北京”,这里 `$city` 变量就接受到并赋值为“北京”了,代码会继续执行,和直接一次性发送机器人“天气 北京”是一个效果,此为框架封装的消息会话机制,以贴近自然会话的方式来编写代码逻辑。
最后,函数直接返回了一个字符串,作为事件的响应,炸毛框架会自动处理并调用机器人客户端的接口,最后返回给用户消息。这里也可以使用上下文的 `ctx()->reply("xxx")` 方法替代,不返回字符串。注意两者只能选择一种方式,取决于开发者的开发习惯。
<chat-box>
) 天气 北京
( 北京 天气情况2020年12月22日-2~9℃ blablabla
) 天气
( 请告诉我你要查询的城市
) 北京
( 北京 天气情况2020年12月22日-2~9℃ blablabla
</chat-box>

View File

@@ -0,0 +1,68 @@
# 编写模块
到现在为止,我们还在使用框架的默认模块 `Example/Hello.php`,在开始编写自己的模块应用之前,我们先说明一些编写代码的约定。
## 加载模块
框架默认使用脚手架构建好后,目录结构大致为下面这样:
```bash
zhamao-framework-starter/
├── config/ # 项目的配置文件文件夹,如 global.php
├── src/ # 项目的主要源码目录
│ ├── Module/ # 用户编写的模块目录
│ │ └── Example/ # 模块文件夹名称
│ │ └── Hello.php # 模块内的类
│ └── Custom/ # 用户自定义的全局方法、全局注解类等存放的目录
├── vendor/ # Composer 依赖加载目录
└── composer.json # Composer 配置文件
```
其中我们脚手架包含的默认模块 `Example` 下的 `Hello` 类,就是用户写模块的位置。你也可以根据实际情况,自行添加更多的模块文件夹甚至单文件模块。
需要注意的是,所有文件夹名称和 `.php` 文件必须遵循 [psr-4 规范](https://learnku.com/docs/psr/psr-4-autoloader/1608),简单来说,`src/` 目录下的文件夹,子文件夹要写成命名空间,比如默认框架中 `Example/` 下的 `.php` 文件的命名空间为 `namespace Module\Example;`,且一个 `.php` 文件推荐只包含一个 `class``trait``interface`
```php
<?php
namespace Module\<your-module-dir>;
class ModuleA {}
```
!!! fail "警告"
如果没有遵守上方的类和文件命名规则的话(文件名、文件夹名和命名空间的统一性),在加载框架时就会报错,无法找到对应的类。因为框架的注解解析依赖于 Composer 中 psr-4 规则的自动加载。
## 创建模块
### 标准形式
我们这里以 `Entertain` 娱乐模块的创建为例,新建一个内有 `Dice.php` 掷骰子功能的模块,目录结构如下,在 `Module/` 下新建文件夹 `Entertain/`,再在此子目录下新建 `Dice.php` 文件。
```bash
zhamao-framework-starter/
└── src/
└── Module/
└── Entertain/
└── Dice.php
```
新建的 PHP 文件按照如下方式编写:
```php
<?php
namespace Module\Entertain;
class Dice {
}
```
这个时候它已经可以被称为一个模块了,尽管它还什么都没做。
### 单文件形式
如果你只开发很简单的一些功能,如一个 PHP 文件就可以实现的,可以少去创建模块文件夹的一步,直接将 `.php` 文件新建到 `Module/` 文件夹下,这时此文件的命名空间需要更正为 `namespace Module;` 即可,而文件夹结构也更加简单:
```bash
zhamao-framework-starter/
└── src/
└── Module/
└── Dice.php
```
### Composer 外部引入形式
(暂未支持,敬请期待)

130
docs/index.md Normal file
View File

@@ -0,0 +1,130 @@
# 介绍
> 本文档为炸毛框架 v2 版本,如需查看 v1 版本,[点我](https://docs-v1.zhamao.xin/)。
> 如果是从 v1.x 版本升级到 v2.x[点我看升级指南](/advanced/to-v2/)。
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
在 HTTP 和 WebSocket 服务器上PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快,未来也会兼容微信公众号开发者模式。
```php
/**
* @CQCommand("你好")
*/
public function hello() {
ctx()->reply("你好,我是炸毛!");
}
/**
* @RequestMapping("/index")
*/
public function index() {
return "<h1>hello!</h1>";
}
```
## 开始前
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行基础
2. php 7.2+ 开发环境
3. HTTP 协议(可选)
4. OneBot 机器人聊天接口标准(可选)
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。
!!! bug "文档提示"
此文档采用 MkDocs 驱动,但因为本文档的搜索组件原生不支持中文搜索,所以搜索体验会大打折扣,敬请谅解!搜不到不是没这个东西哦!
## 框架特色
- 支持MySQL数据库连接池自带查询缓存提高多查询时的效率
- Websocket 服务器、HTTP 服务器兼容运行,一个框架多个用处
- 支持命令、自然语言处理等多种插件形式
- 支持多个机器人账号负载均衡
- 协程 + TaskWorker 进程重度任务处理机制,保证高效,单个请求响应时间为 0.1 ms 左右
- 模块分离和自由组合,可根据自身需求自己建立模块内的目录结构和代码结构
- 灵活的注释注解注册事件方式,弥补 PHP 语言缺少注解的遗憾
## 文档主题
### 主题
<div class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</div>
<script>
var buttons = document.querySelectorAll("button[data-md-color-scheme]");
buttons.forEach(function(button) {
button.addEventListener("click", function() {
var attr = this.getAttribute("data-md-color-scheme");
setCookie("_theme", attr);
document.body.setAttribute("data-md-color-scheme", attr);
var name = document.querySelector("#__code_0 code span:nth-child(7)");
name.textContent = attr;
})
})
</script>
### 主色调
<div class="tx-switch">
<button data-md-color-primary="red"><code>red</code></button>
<button data-md-color-primary="pink"><code>pink</code></button>
<button data-md-color-primary="purple"><code>purple</code></button>
<button data-md-color-primary="deep-purple"><code>deep purple</code></button>
<button data-md-color-primary="indigo"><code>indigo</code></button>
<button data-md-color-primary="blue"><code>blue</code></button>
<button data-md-color-primary="light-blue"><code>light blue</code></button>
<button data-md-color-primary="cyan"><code>cyan</code></button>
<button data-md-color-primary="teal"><code>teal</code></button>
<button data-md-color-primary="green"><code>green</code></button>
<button data-md-color-primary="light-green"><code>light green</code></button>
<button data-md-color-primary="lime"><code>lime</code></button>
<button data-md-color-primary="yellow"><code>yellow</code></button>
<button data-md-color-primary="amber"><code>amber</code></button>
<button data-md-color-primary="orange"><code>orange</code></button>
<button data-md-color-primary="deep-orange"><code>deep orange</code></button>
<button data-md-color-primary="brown"><code>brown</code></button>
<button data-md-color-primary="grey"><code>grey</code></button>
<button data-md-color-primary="blue-grey"><code>blue grey</code></button>
<button data-md-color-primary="black"><code>black</code></button>
<button data-md-color-primary="white"><code>white</code></button>
</div>
### 辅色调
<div class="tx-switch"> <button data-md-color-accent="red"><code>red</code></button> <button data-md-color-accent="pink"><code>pink</code></button> <button data-md-color-accent="purple"><code>purple</code></button> <button data-md-color-accent="deep-purple"><code>deep purple</code></button> <button data-md-color-accent="indigo"><code>indigo</code></button> <button data-md-color-accent="blue"><code>blue</code></button> <button data-md-color-accent="light-blue"><code>light blue</code></button> <button data-md-color-accent="cyan"><code>cyan</code></button> <button data-md-color-accent="teal"><code>teal</code></button> <button data-md-color-accent="green"><code>green</code></button> <button data-md-color-accent="light-green"><code>light green</code></button> <button data-md-color-accent="lime"><code>lime</code></button> <button data-md-color-accent="yellow"><code>yellow</code></button> <button data-md-color-accent="amber"><code>amber</code></button> <button data-md-color-accent="orange"><code>orange</code></button> <button data-md-color-accent="deep-orange"><code>deep orange</code></button> </div>
<script>
var buttons = document.querySelectorAll("button[data-md-color-primary]")
buttons.forEach(function(button) {
button.addEventListener("click", function() {
var attr = this.getAttribute("data-md-color-primary")
setCookie("_primary_color", attr)
document.body.setAttribute("data-md-color-primary", attr)
var name = document.querySelector("#__code_2 code span:nth-child(7)")
name.textContent = attr.replace("-", " ")
})
})
</script>
<script>
var buttons2 = document.querySelectorAll("button[data-md-color-accent]")
buttons2.forEach(function(button) {
button.addEventListener("click", function() {
var attr = this.getAttribute("data-md-color-accent")
setCookie("_accent_color", attr)
document.body.setAttribute("data-md-color-accent", attr)
var name = document.querySelector("#__code_3 code span:nth-child(7)")
name.textContent = attr.replace("-", " ")
})
})
</script>

View File

@@ -0,0 +1,85 @@
hljs.initHighlighting()
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?f0f276cefa10aa31a20ae3815a50b795";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
function appendChatModule(id, chatDialogs) {
let insertDiv = document.getElementById(id);
let ss = '';
ss += '<div class="doc-chat-container">';
for(let i of chatDialogs) {
if (i.role === 0) {
ss += '<div class="doc-chat-row doc-chat-row-robot">\n' +
' <img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>\n' +
' <div class="doc-chat-box doc-chat-box-robot">' + i.msg + '</div>\n' +
' </div>';
} else {
ss += '<div class="doc-chat-row">\n' +
' <div class="doc-chat-box">' + i.msg + '</div>\n' +
' <img class="doc-chat-avatar" src="http://api.btstu.cn/sjtx/api.php" alt=""/>\n' +
' </div>';
}
}
insertDiv.innerHTML = ss + '</div>';
}
function getCookie(name) {
var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
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();
}
s_theme=getCookie("_theme");
if(s_theme === undefined) s_theme = "default";
document.body.setAttribute("data-md-color-scheme", s_theme)
var name = document.querySelector("#__code_0 code span:nth-child(7)")
name.textContent = s_theme
s_primary=getCookie("_primary_color");
document.body.setAttribute("data-md-color-primary", s_primary);
var name2 = document.querySelector("#__code_2 code span:nth-child(7)");
if(s_primary !== null && name2 !== null) name2.textContent = s_primary.replace("-", " ");
s_accent=getCookie("_accent_color");
document.body.setAttribute("data-md-color-accent", s_accent);
var name3 = document.querySelector("#__code_3 code span:nth-child(7)");
if(s_accent !== null && name3 !== null) name3.textContent = s_accent.replace("-", " ");
setTimeout(() => {
let ls = document.querySelectorAll("chat-box");
for(let i of ls) {
let final = '<div class="doc-chat-container">';
let dialogs = i.innerHTML.split("\n");
for(let j of dialogs) {
if(j === '') continue;
if(j.substr(0, 2) === ') ') {
final += '<div class="doc-chat-row">\n' +
' <div class="doc-chat-box">' + j.substr(2) + '</div>\n' +
' <img class="doc-chat-avatar" src="http://api.btstu.cn/sjtx/api.php" alt=""/>\n' +
' </div>';
} else if (j.substr(0, 2) === '( ') {
final += '<div class="doc-chat-row doc-chat-row-robot">\n' +
' <img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>\n' +
' <div class="doc-chat-box doc-chat-box-robot">' + j.substr(2) + '</div>\n' +
' </div>';
}
}
i.innerHTML = final;
}
}, 500);

220
docs/update/v1.md Normal file
View File

@@ -0,0 +1,220 @@
# 更新日志v1 版本)
## v1.6.5
> 更新时间2020.12.9
- 修复:版本号显示
- 优化:依赖问题,减少对 PHP 扩展的依赖,转变为可选
## v1.6.4
> 更新时间2020.12.9
- 修复composer require模式下自动加载的问题
- 优化:减少不是必需的依赖问题
## v1.6.3
> 更新时间2020.11.15
- 修复Response 对象使用 redirect 造成的递归报错
- 修复:`document_index` 配置项在 `/` 路径下无法使用的 bug
## v1.6.2
> 更新时间2020.7.27
- 修复:不写配置 `server_event_handler_class` 项无法启动的 bug
## v1.6.1
> 更新时间2020.7.26
- 新增:`ZMRequest::downloadFile($url, $dst)` 方法,可直接将文件下载到本地
## v1.6
> 更新时间2020.7.14
- 新增:现在可以对类修饰自定义的注解了
- 修复:数据库操作 where 对象时产生的歧义
- 新增:支持自定义任何 Swoole Server 事件的注解绑定,详见文档
- 修复:多个中间件注解对类只生效最后一个的 bug
❗ 下面是框架升级需要手动进行的变更:
- 新版本由于引进了自定义 Swoole Server 事件的机制,对 global.php 全局配置文件有了变动,需要添加以下内容才能正常启动(旧版本升级新版本用户,新用户无需操作):
```php
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\Framework\ServerEventHandler::class, //默认不可删除,否则会不能使用框架
];
```
## v1.5.8
> 更新时间2020.6.26
- 新增:`@CQCommand` 注解的 fullMatch 参数(全量正则表达式匹配)
## v1.5.7
> 更新时间2020.6.20
- 新增ZM_BREAKPOINT 的短名称BP
- 优化:终端连接器自动重连
- 修复:语法错误时防止循环报错
## v1.5.6
> 更新时间2020.6.15
- 新增:`@CQCommand` 注解支持 `message_type``user_id``group_id``discuss_id` 限定条件
- 新增PDO 数据库支持自定义 fetch_mode可在 `global.php` 中的 `sql_config["sql_default_fetch_mode"]` 字段设置,也可以调用时 `DB::rawQuery("语句", [], PDO::FETCH_ASSOC);` 第三个参数可选
- 🔴 废弃:`ModBase` 基类,基类继承机制将在 1.6 版本起完全删除
## v1.5.5
> 更新时间2020.6.13
- 修复:`@SwooleEventAt("close")` 下不能使用 `ctx()->getConnection()` 获取链接对象的 bug
- 新增init 命令,可在 `composer require zhamao/framework` 后使用 `vendor/bin/start init` 初始化项目目录结构和配置文件
- 更新:默认模块新增机器人断开连接的回调事件
## v1.5.4
> 更新时间2020.6.13
- 新增:`@CQCommand` 下支持 alias 参数
- 更新:将 autoload 变为 composer autoload需要重新 composer update
## v1.5.3
> 更新时间2020.6.10
- 修复:在 Linux 系统下 Terminal 无法正常使用的 bug
## v1.5.2
> 更新时间2020.6.8
- 新增:`ZM_VERSION` 常量,对应为当前框架版本
- 修复:部分链接不带 `/` 会导致 ZMRequest 模块报错的 bug
## v1.5.1
> 更新时间2020.6.5
- 新增ZMRequest::request() 自定义构建 HTTP 请求方法
- 修复:一个不会导致崩溃的 warning 提示
## v1.5
> 更新时间2020.6.5
- 重要变更:支持从 composer 使用框架
- 新增:数据库 Select 选择器支持 `count()` 方法
- 修复ZMRequest 中 https 和端口的指定顺序问题
- 新增ZMWebSocket 创建 WS 链接的轻量级客户端
- 修复:数据库异常的捕获更改为 PDOException
## v1.4
> 更新时间2020.5.23
- 新增:自定义 motd
- 新增debug_mode 下断点调试功能
- 新增:`@OnSave` 注解,储存自动保存的变量时事件激活
- 新增Swoole 版本检测
- 新增:全局函数,以 `zm_` 开头的,详情见文档
- 新增:`@LoadBuffer` 注解,只加载内存不自动保存的变量
- 新增:局部静态文件服务
- 新增mysqlnd 扩展状态检测
- 更新:将终端输入更换为多进程
- 更新:将数据库连接池变更为 Swoole 官方的连接池,需要 Swoole 版本 >= 4.4.13
- 更新:提升注解绑定的事件函数的执行效率
- 修复:上下文 `getConnection()` 的 fd 无法获取的 bug
- 修复MySQL 长链接 gone away 自动重连的问题
- 修复MySQL 查询构造器无 WHERE 语句时会造成的 bug
- 修复:调整各项资源初始化前后顺序
不可逆修改:你需要重新执行一次 `composer update` 或重新拉取一次 Docker Image因为 composer 依赖发生了变化。
## v1.3.1
> 更新时间2020.5.10
- 修复DataProvider 下 setJsonData 新建文件夹的问题
- 优化:默认 / 页面显示 `Hello Zhamao!` 文字
- 优化Exception 和 Fatal error 报错机制的改进
- 修复:计时器没有上下文环境,发不了 API 的 bug
❗ 下面是框架升级需要手动进行的变更:
- 更改 MySQL 客户端为原生 PDO mysqlnd如果之前使用 Docker 启动,则需使用新的 Dockerfile 构建。如果安装在本机,需安装 php-mysql 扩展。本次更新不影响框架内的 API不需要更改任何代码。
## v1.3.0
> 更新时间2020.5.8
- 新增:上下文,具体更新都写到了文档里了!
- 修复ZMRobot 的 `setPrefix()` 的严重错误
- 优化:优化部分代码
- 改动:现在你可以和任意事件的注解使用任意中间件啦,而且还支持多中间件
- 新增CQHTTP + 酷Q + 炸毛框架 的 Dockerfile
- 新增注解:`@CQAPISend``@CQAPIResponse`,是 API 调用后触发的事件,具体见文档说明
## v1.2.1
> 更新时间2020.5.2
- 新增phar 启动模式构建脚本,你可以直接拉取 phar 运行框架了!
- 优化:优化部分代码
## v1.2
> 更新时间2020.4.29
- 新增systemd 生成脚本、一键 daemonize 守护进程方式常驻后台
- 新增:示例模块的注释
- 重构Console 模块,现在有准确的控制台输出分级功能了
- 新增:`@OnTick` 注解,用于绑定定时器(毫秒级)
- 新增:`ZMRobot` 类,比调用 `CQAPI` 类发送 API 更方便,同时兼容最新版本的 `CQHTTP` 插件
- 优化:使用键盘中断 `Ctrl+C`,不会丢失未保存的缓存数据了
- 优化:完善上下文对象的方法
- 新增:终端命令:`logtest`,测试输出的 log 类型
:exclamation:下面是框架模块开发中需要注意的或有不兼容的修改内容:
- 修改:`global.php` 中原来的 `info_level` 默认数值需要改为 `2`,保证终端输出和原来一致
## v1.1.2
> 更新时间2020.4.26
- 新增:静态文件服务器
- 修复:`/` 路径的 Mapping 无法正常绑定的 bug
## v1.1.1
> 更新时间2020.4.26
- 新增:中间件对类的修饰
- 新增:上下文对象对 IDE 的支持
- 修复:数据库插入查询的愚蠢错误
- 修复:数据库查询的 `value()` 不支持指定参数的 bug
## v1.1.0
> 更新时间2020.3.29
- 新增:中间件 `@Middleware` 功能
- 修复Websocket 链接关闭后未自动删除连接对象的bug
## v1.0.0
> 更新时间2020.3.19
正式版发布。

3
docs/update/v2.md Normal file
View File

@@ -0,0 +1,3 @@
# 更新日志v2 版本)
> 暂未发布正式版。

74
mkdocs.yml Normal file
View File

@@ -0,0 +1,74 @@
site_name: 炸毛框架 v2
repo_name: '炸毛框架'
repo_url: 'https://github.com/zhamao-robot/zhamao-framework'
edit_uri: 'blob/2.0-dev/docs/'
theme:
name: material
logo: assets/logos.png
favicon: assets/favicon.png
language: zh
features:
- navigation.tabs
extra_javascript:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
- javascripts/config.js
extra_css:
- assets/css/extra.css
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
markdown_extensions:
- admonition
- pymdownx.tabbed
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- abbr
- pymdownx.highlight:
linenums: true
linenums_style: pymdownx.inline
extra:
version:
method: mike
copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</span>
<script>
var buttons = document.querySelectorAll("button[data-md-color-scheme]");
buttons.forEach(function(button) {
button.addEventListener("click", function() {
var attr = this.getAttribute("data-md-color-scheme");
setCookie("_theme", attr);
document.body.setAttribute("data-md-color-scheme", attr);
var name = document.querySelector("#__code_0 code span:nth-child(7)");
name.textContent = attr;
})
})
</script><br><a href="http://beian.miit.gov.cn">蒙ICP备18000198号-1</a>'
nav:
- 指南:
- 介绍: index.md
- 安装框架: guide/安装.md
- 快速上手(机器人篇): guide/快速上手-机器人.md
- 快速上手HTTP篇: guide/快速上手-http.md
- 选择聊天机器人实例: guide/OneBot实例.md
- 基本配置: guide/基本配置.md
- 编写模块: guide/编写模块.md
- 注册事件响应: guide/注册事件响应.md
- 事件和注解:
- 事件和注解: event/index.md
- 框架组件:
- 框架组件: component/index.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 从 v1 升级: advanced/to-v2.md
- FAQ:
- FAQ: FAQ.md
- 更新日志:
- 更新日志v2: update/v2.md
- 更新日志v1: update/v1.md
- <u>炸毛框架 v1</u>: https://docs-v1.zhamao.xin/

View File

@@ -1,85 +0,0 @@
<?php
global $is_phar;
use Framework\FrameworkLoader;
$is_phar = true;
if (substr(__DIR__, 0, 7) != 'phar://') {
die("You can not run this script directly!\n");
}
testEnvironment();
spl_autoload_register(function ($class) {
//echo $class."\n";
$exp = str_replace("\\", '/', $class);
$exp = __DIR__ . '/src/' . $exp . '.php';
if (is_file($exp)) {
require_once $exp;
}
});
loadPhp(__DIR__ . '/src');
Swoole\Coroutine::set([
'max_coroutine' => 30000,
]);
date_default_timezone_set("Asia/Shanghai");
define('WORKING_DIR', __DIR__);
define('FRAMEWORK_DIR', __DIR__);
define('LOAD_MODE', 2);
$s = new FrameworkLoader($argv);
function loadPhp($dir) {
$dirs = scandir($dir);
foreach ($dirs as $v) {
$path = $dir . '/' . $v;
if (is_dir($path)) {
loadPhp($path);
} else {
if (pathinfo($dir . '/' . $v)['extension'] == 'php') {
if(pathinfo($dir . '/' . $v)['basename'] == 'terminal_listener.php') continue;
//echo 'loading '.$path.PHP_EOL;
require_once $path;
}
}
}
}
function testEnvironment() {
$current_dir = realpath('.');
@mkdir($current_dir . '/config/');
if (!is_file($current_dir . '/config/global.php')) {
echo "Exporting default global config...\n";
$global = file_get_contents(__DIR__ . '/config/global.php');
$global = str_replace("WORKING_DIR", 'realpath(__DIR__ . "/../")', $global);
file_put_contents($current_dir . '/config/global.php', $global);
}
if (!is_file($current_dir . '/config/file_header.json')) {
echo "Exporting default file_header config...\n";
$global = file_get_contents(__DIR__ . '/config/file_header.json');
file_put_contents($current_dir . '/config/file_header.json', $global);
}
if (!is_dir($current_dir . '/resources')) mkdir($current_dir . '/resources');
if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src');
if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src');
if (!is_dir($current_dir . '/src/Module')) {
mkdir($current_dir . '/src/Module');
mkdir($current_dir . '/src/Module/Example');
file_put_contents($current_dir . '/src/Module/Example/Hello.php', file_get_contents(__DIR__ . '/tmp/Hello.php.bak'));
mkdir($current_dir . '/src/Module/Middleware');
file_put_contents($current_dir . '/src/Module/Middleware/TimerMiddleware.php', file_get_contents(__DIR__ . '/tmp/TimerMiddleware.php.bak'));
}
if (!is_dir($current_dir . '/src/Custom')) {
mkdir($current_dir . '/src/Custom');
mkdir($current_dir . '/src/Custom/Annotation');
mkdir($current_dir . '/src/Custom/Connection');
file_put_contents($current_dir . '/src/Custom/global_function.php', "<?php\n\n//这里写你的全局方法");
}
}

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example page</title>
</head>
<body>
<div style="background: red; width: 100px; height: 100px"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -4,6 +4,7 @@
namespace Custom\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\CustomAnnotation;
/**
@@ -12,8 +13,8 @@ use ZM\Annotation\Interfaces\CustomAnnotation;
* @Target("ALL")
* @package Custom\Annotation
*/
class Example implements CustomAnnotation
class Example extends AnnotationBase implements CustomAnnotation
{
/** @var string */
public $str;
}
public $str = '';
}

View File

@@ -0,0 +1,31 @@
<?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;
}
}
}

View File

@@ -1,14 +0,0 @@
<?php
namespace Custom\Connection;
use ZM\Connection\WSConnection;
class CustomConnection extends WSConnection
{
public function getType() {
return "custom";
}
}

View File

@@ -1,3 +1,6 @@
<?php
//这里写你的全局函数
function pgo(callable $func, $name = "default") {
\ZM\Utils\CoroutinePool::go($func, $name);
}

View File

@@ -1,277 +0,0 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2018/2/10
* Time: 下午6:13
*/
namespace Framework;
use ZM\Annotation\Swoole\SwooleEventAt;
use ZM\Connection\WSConnection;
use ZM\Utils\ZMUtil;
use Exception;
class Console
{
/**
* @var false|resource
*/
public static $console_proc = null;
public static $pipes = [];
static function setColor($string, $color = "") {
switch ($color) {
case "red":
return "\x1b[38;5;203m" . $string . "\x1b[m";
case "green":
return "\x1b[38;5;83m" . $string . "\x1b[m";
case "yellow":
return "\x1b[38;5;227m" . $string . "\x1b[m";
case "blue":
return "\033[34m" . $string . "\033[0m";
case "pink": // I really don't know what stupid color it is.
case "lightpurple":
return "\x1b[38;5;207m" . $string . "\x1b[m";
case "lightblue":
return "\x1b[38;5;87m" . $string . "\x1b[m";
case "gold":
return "\x1b[38;5;214m" . $string . "\x1b[m";
case "gray":
return "\x1b[38;5;59m" . $string . "\x1b[m";
case "lightlightblue":
return "\x1b[38;5;63m" . $string . "\x1b[m";
default:
return $string;
}
}
static function error($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[E] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "red") . "\n");
}
static function warning($obj, $head = null) {
if ($head === null) $head = date("[H:i:s]") . " [W] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 1) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "blue" : "yellow") . "\n");
}
}
static function info($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[I] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 2) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "black" : "lightblue") . "\n");
}
}
static function success($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[S] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 2) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "green") . "\n");
}
}
static function verbose($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[V] ";
if (ZMBuf::$atomics["info_level"]->get() >= 3) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "blue") . "\n");
}
}
static function debug($msg) {
if (ZMBuf::$atomics["info_level"]->get() >= 4) Console::log(date("[H:i:s] ") . "[D] " . $msg, 'gray');
}
static function log($obj, $color = "") {
if (!is_string($obj)) var_dump($obj);
else echo(self::setColor($obj, $color) . "\n");
}
static function stackTrace() {
$log = "Stack trace:\n";
$trace = debug_backtrace();
//array_shift($trace);
foreach ($trace as $i => $t) {
if (!isset($t['file'])) {
$t['file'] = 'unknown';
}
if (!isset($t['line'])) {
$t['line'] = 0;
}
if (!isset($t['function'])) {
$t['function'] = 'unknown';
}
$log .= "#$i {$t['file']}({$t['line']}): ";
if (isset($t['object']) and is_object($t['object'])) {
$log .= get_class($t['object']) . '->';
}
$log .= "{$t['function']}()\n";
}
$log = Console::setColor($log, "gray");
echo $log;
}
static function listenConsole() {
if (in_array('--disable-console-input', FrameworkLoader::$argv) || in_array('--debug-mode', FrameworkLoader::$argv)) {
self::info("ConsoleCommand disabled.");
return;
}
global $terminal_id;
global $port;
$port = ZMBuf::globals("port");
$vss = new SwooleEventAt();
$vss->type = "open";
$vss->level = 256;
$vss->rule = "connectType:terminal";
$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)));
});
$vss->callback = function(?WSConnection $conn) use ($terminal_id){
$req = ctx()->getRequest();
if($conn->getType() != "terminal") return false;
Console::debug("Terminal fd: ".$conn->fd);
if(($req->header["x-terminal-id"] ?? "") != $terminal_id) {
$conn->close();
return false;
}
return false;
};
ZMBuf::$events[SwooleEventAt::class][] = $vss;
$vss2 = new SwooleEventAt();
$vss2->type = "message";
$vss2->rule = "connectType:terminal";
$vss2->callback = function(?WSConnection $conn){
if ($conn === null) return false;
if($conn->getType() != "terminal") return false;
$cmd = ctx()->getFrame()->data;
self::executeCommand($cmd);
return false;
};
ZMBuf::$events[SwooleEventAt::class][] = $vss2;
go(function () {
global $terminal_id, $port;
$descriptorspec = array(
0 => STDIN,
1 => STDOUT,
2 => STDERR
);
self::$console_proc = proc_open('php -r \'$terminal_id = "'.$terminal_id.'";$port = '.$port.';require "'.__DIR__.'/terminal_listener.php";\'', $descriptorspec, $pipes);
});
}
/**
* @param string $cmd
* @return bool
*/
private static function executeCommand(string $cmd) {
$it = explodeMsg($cmd);
switch ($it[0] ?? '') {
case 'logtest':
Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)");
Console::error("This is error msg. (0)");
Console::warning("This is warning msg. (1)");
Console::info("This is info msg. (2)");
Console::success("This is success msg. (2)");
Console::verbose("This is verbose msg. (3)");
Console::debug("This is debug msg. (4)");
return true;
case 'call':
$class_name = $it[1];
$function_name = $it[2];
$class = new $class_name([]);
call_user_func_array([$class, $function_name], []);
return true;
case 'bc':
$code = base64_decode($it[1] ?? '', true);
try {
eval($code);
} catch (Exception $e) {
}
return true;
case 'echo':
Console::info($it[1]);
return true;
case 'color':
Console::log($it[2], $it[1]);
return true;
case 'stop':
ZMUtil::stop();
return false;
case 'reload':
case 'r':
ZMUtil::reload();
return false;
case 'save':
$origin = ZMBuf::$atomics["info_level"]->get();
//ZMBuf::$atomics["info_level"]->set(3);
DataProvider::saveBuffer();
//ZMBuf::$atomics["info_level"]->set($origin);
return true;
case '':
return true;
default:
Console::info("Command not found: " . $cmd);
return true;
}
}
public static function withSleep(string $string, int $int) {
self::info($string);
sleep($int);
}
}

View File

@@ -1,78 +0,0 @@
<?php
namespace Framework;
use ZM\Annotation\Swoole\OnSave;
class DataProvider
{
public static $buffer_list = [];
public static function getResourceFolder() {
return self::getWorkingDir() . '/resources/';
}
public static function getWorkingDir() {
if(LOAD_MODE == 0) return WORKING_DIR;
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
elseif (LOAD_MODE == 2) return realpath('.');
return null;
}
public static function getDataConfig() {
return CONFIG_DIR;
}
public static function addSaveBuffer($buf_name, $sub_folder = null) {
$name = ($sub_folder ?? "") . "/" . $buf_name . ".json";
self::$buffer_list[$buf_name] = $name;
Console::debug("Added " . $buf_name . " at $sub_folder");
ZMBuf::set($buf_name, self::getJsonData($name));
}
public static function saveBuffer() {
$head = Console::setColor(date("[H:i:s] ") . "[V] Saving buffer......", "blue");
if (ZMBuf::$atomics["info_level"]->get() >= 3)
echo $head;
foreach (self::$buffer_list as $k => $v) {
Console::debug("Saving " . $k . " to " . $v);
self::setJsonData($v, ZMBuf::get($k));
}
foreach (ZMBuf::$events[OnSave::class] ?? [] as $v) {
$c = $v->class;
$method = $v->method;
$class = new $c();
Console::debug("Calling @OnSave: $c -> $method");
$class->$method();
}
if (ZMBuf::$atomics["info_level"]->get() >= 3)
echo Console::setColor("saved", "blue") . PHP_EOL;
}
public static function getFrameworkLink() {
return ZMBuf::globals("http_reverse_link");
}
public static function getJsonData(string $string) {
if (!file_exists(self::getDataConfig() . $string)) return [];
return json_decode(file_get_contents(self::getDataConfig() . $string), true);
}
public static function setJsonData($filename, array $args) {
$pathinfo = pathinfo($filename);
if (!is_dir(self::getDataConfig() . $pathinfo["dirname"])) {
Console::debug("Making Directory: " . self::getDataConfig() . $pathinfo["dirname"]);
mkdir(self::getDataConfig() . $pathinfo["dirname"]);
}
$r = file_put_contents(self::getDataConfig() . $filename, json_encode($args, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING));
if ($r === false) {
Console::warning("无法保存文件: " . $filename);
}
}
public static function getDataFolder() {
return ZM_DATA;
}
}

View File

@@ -1,193 +0,0 @@
<?php
namespace Framework;
use Doctrine\Common\Annotations\AnnotationReader;
use ReflectionClass;
use ReflectionMethod;
use Swoole\Runtime;
use ZM\Annotation\Swoole\OnEvent;
use Exception;
use Swoole\WebSocket\Server;
/**
* Class FrameworkLoader
* Everything is beginning from here
* @package Framework
*/
class FrameworkLoader
{
/** @var GlobalConfig */
public static $settings;
/** @var FrameworkLoader|null */
public static $instance = null;
/** @var float|string */
public static $run_time;
/**
* @var array
*/
public static $argv;
/** @var Server */
private $server;
public function __construct($args = []) {
$this->requireGlobalFunctions();
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;
//$this->registerAutoloader('classLoader');
require_once "DataProvider.php";
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
/** @noinspection PhpIncludeInspection */
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
}
if (LOAD_MODE == 2) {
require_once FRAMEWORK_DIR . "/vendor/autoload.php";
spl_autoload_register('phar_classloader');
}
self::$settings = new GlobalConfig();
if (self::$settings->get("debug_mode") === true) {
$args[] = "--debug-mode";
$args[] = "--disable-console-input";
}
self::$argv = $args;
if (!in_array("--debug-mode", self::$argv)) {
Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
}
self::$settings = new GlobalConfig();
ZMBuf::$globals = self::$settings;
if (!self::$settings->success) die("Failed to load global config. Please check config/global.php file");
$this->defineProperties();
//start swoole Framework
$this->selfCheck();
try {
$this->server = new Server(self::$settings->get("host"), self::$settings->get("port"));
$settings = self::$settings->get("swoole");
if (in_array("--daemon", $args)) {
$settings["daemonize"] = 1;
Console::log("已启用守护进程,输出重定向到 " . $settings["log_file"]);
self::$argv[] = "--disable-console-input";
}
$this->server->set($settings);
$all_event_class = self::$settings->get("server_event_handler_class");
$event_list = [];
foreach ($all_event_class as $v) {
$reader = new AnnotationReader();
$reflection_class = new ReflectionClass($v);
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $vs) {
$method_annotations = $reader->getMethodAnnotations($vs);
if ($method_annotations != []) {
$annotation = $method_annotations[0];
if ($annotation instanceof OnEvent) {
$annotation->class = $v;
$annotation->method = $vs->getName();
$event_list[strtolower($annotation->event)] = $annotation;
}
}
}
}
foreach ($event_list as $k => $v) {
$this->server->on($k, function (...$param) use ($v) {
$c = $v->class;
//echo $c.PHP_EOL;
$c = new $c();
call_user_func_array([$c, $v->method], $param);
});
}
ZMBuf::initAtomic();
if (in_array("--remote-shell", $args)) RemoteShell::listen($this->server, "127.0.0.1");
if (in_array("--log-error", $args)) ZMBuf::$atomics["info_level"]->set(0);
if (in_array("--log-warning", $args)) ZMBuf::$atomics["info_level"]->set(1);
if (in_array("--log-info", $args)) ZMBuf::$atomics["info_level"]->set(2);
if (in_array("--log-verbose", $args)) ZMBuf::$atomics["info_level"]->set(3);
if (in_array("--log-debug", $args)) ZMBuf::$atomics["info_level"]->set(4);
Console::log(
"host: " . self::$settings->get("host") .
", port: " . self::$settings->get("port") .
", log_level: " . ZMBuf::$atomics["info_level"]->get() .
", version: " . ZM_VERSION .
"\nworking_dir: " . DataProvider::getWorkingDir()
);
global $motd;
if (!file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
echo $motd;
} else {
echo file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
}
if (in_array("--debug-mode", self::$argv))
Console::warning("You are in debug mode, do not use in production!");
register_shutdown_function(function() {
$error = error_get_last();
if(isset($error["type"]) && $error["type"] == 1) {
if(mb_strpos($error["message"], "require") !== false && mb_strpos($error["message"], "callback") !== false) {
echo "\e[38;5;203mYou may need to update your \"global.php\" config!\n";
echo "Please see: https://github.com/zhamao-robot/zhamao-framework/issues/15\e[m\n";
}
}
});
$this->server->start();
} catch (Exception $e) {
Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage());
die;
}
}
private function requireGlobalFunctions() {
require_once __DIR__ . '/global_functions.php';
}
private function defineProperties() {
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", self::$settings->get("zm_data"));
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
define("CONFIG_DIR", self::$settings->get("config_dir"));
define("CRASH_DIR", self::$settings->get("crash_dir"));
@mkdir(ZM_DATA);
@mkdir(CONFIG_DIR);
@mkdir(CRASH_DIR);
define("ZM_MATCH_ALL", 0);
define("ZM_MATCH_FIRST", 1);
define("ZM_MATCH_NUMBER", 2);
define("ZM_MATCH_SECOND", 3);
define("ZM_BREAKPOINT", 'if(in_array("--debug-mode", \Framework\FrameworkLoader::$argv)) extract(\Psy\debug(get_defined_vars(), isset($this) ? $this : @get_called_class()));');
define("BP", ZM_BREAKPOINT);
define("ZM_DEFAULT_FETCH_MODE", self::$settings->get("sql_config")["sql_default_fetch_mode"] ?? 4);
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\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 (!extension_loaded("ctype")) die("Can not find ctype extension.\n");
if (!function_exists("mb_substr")) die("Can not find mbstring 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");
return true;
}
}
global $motd;
$motd = <<<EOL
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
EOL;

View File

@@ -1,37 +0,0 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2019-03-16
* Time: 13:58
*/
namespace Framework;
/**
* 请不要diss此class的语法。可能写的很糟糕。
* Class GlobalConfig
*/
class GlobalConfig
{
private $config = null;
public $success = false;
public function __construct() {
/** @noinspection PhpIncludeInspection */
include_once DataProvider::getWorkingDir() . '/config/global.php';
global $config;
$this->success = true;
$this->config = $config;
}
public function get($key) {
$r = $this->config[$key] ?? null;
if ($r === null) return null;
return $r;
}
public function getAll() {
return $this->config;
}
}

View File

@@ -1,264 +0,0 @@
<?php
namespace Framework;
use Co;
use Exception;
use Swoole\Coroutine;
use swoole\server;
class RemoteShell
{
const STX = "DEBUG";
private static $contexts = array();
static $oriPipeMessageCallback = null;
/**
* @var server
*/
static $serv;
static $menu = array(
"p|print [variant]\t打印一个PHP变量的值",
"e|exec [code]\t执行一段PHP代码",
"w|worker [id]\t切换Worker进程",
"l|list\t打印服务器所有连接的fd",
"s|stats\t打印服务器状态",
"c|coros\t打印协程列表",
"b|bt\t打印协程调用栈",
"i|info [fd]\t显示某个连接的信息",
"h|help\t显示帮助界面",
"q|quit\t退出终端",
);
const PAGESIZE = 20;
/**
* @param $serv server
* @param string $host
* @param int $port
* @throws Exception
* @throws Exception
*/
static function listen($serv, $host = "127.0.0.1", $port = 9599) {
Console::warning("正在监听".$host.":".strval($port)."的调试接口,请注意安全");
$port = $serv->listen($host, $port, SWOOLE_SOCK_TCP);
if (!$port) {
throw new Exception("listen fail.");
}
$port->set(array(
"open_eof_split" => true,
'package_eof' => "\r\n",
));
$port->on("Connect", array(__CLASS__, 'onConnect'));
$port->on("Close", array(__CLASS__, 'onClose'));
$port->on("Receive", array(__CLASS__, 'onReceive'));
if (method_exists($serv, 'getCallback')) {
self::$oriPipeMessageCallback = $serv->getCallback('PipeMessage');
}
$serv->on("PipeMessage", array(__CLASS__, 'onPipeMessage'));
self::$serv = $serv;
}
static function onConnect($serv, $fd, $reactor_id) {
self::$contexts[$fd]['worker_id'] = $serv->worker_id;
self::output($fd, implode("\r\n", self::$menu));
}
static function output($fd, $msg) {
if (!isset(self::$contexts[$fd]['worker_id'])) {
$msg .= "\r\nworker#" . self::$serv->worker_id . "$ ";
} else {
$msg .= "\r\nworker#" . self::$contexts[$fd]['worker_id'] . "$ ";
}
self::$serv->send($fd, $msg);
}
static function onClose($serv, $fd, $reactor_id) {
unset(self::$contexts[$fd]);
}
static function onPipeMessage($serv, $src_worker_id, $message) {
//不是 debug 消息
if (!is_string($message) or substr($message, 0, strlen(self::STX)) != self::STX) {
if (self::$oriPipeMessageCallback == null) {
trigger_error("require swoole-4.3.0 or later.", E_USER_WARNING);
return true;
}
return call_user_func(self::$oriPipeMessageCallback, $serv, $src_worker_id, $message);
} else {
$request = unserialize(substr($message, strlen(self::STX)));
self::call($request['fd'], $request['func'], $request['args']);
}
return true ;
}
static protected function call($fd, $func, $args) {
ob_start();
call_user_func_array($func, $args);
self::output($fd, ob_get_clean());
}
static protected function exec($fd, $func, $args) {
//不在当前Worker进程
if (self::$contexts[$fd]['worker_id'] != self::$serv->worker_id) {
self::$serv->sendMessage(self::STX . serialize(['fd' => $fd, 'func' => $func, 'args' => $args]), self::$contexts[$fd]['worker_id']);
} else {
self::call($fd, $func, $args);
}
}
static function getCoros() {
var_export(iterator_to_array(Coroutine::listCoroutines()));
}
static function getBackTrace($_cid) {
$info = Co::getBackTrace($_cid);
if (!$info) {
echo "coroutine $_cid not found.";
} else {
echo get_debug_print_backtrace($info);
}
}
static function printVariant($var) {
$var = ltrim($var, '$ ');
var_dump($var);
var_dump($$var);
}
static function evalCode($code) {
eval($code . ';');
}
/**
* @param $serv server
* @param $fd
* @param $reactor_id
* @param $data
*/
static function onReceive($serv, $fd, $reactor_id, $data) {
$args = explode(" ", $data, 2);
$cmd = trim($args[0]);
unset($args[0]);
switch ($cmd) {
case 'w':
case 'worker':
if (!isset($args[1])) {
self::output($fd, "invalid command.");
break;
}
$dstWorkerId = intval($args[1]);
self::$contexts[$fd]['worker_id'] = $dstWorkerId;
self::output($fd, "[switching to worker " . self::$contexts[$fd]['worker_id'] . "]");
break;
case 'e':
case 'exec':
if (!isset($args[1])) {
self::output($fd, "invalid command.");
break;
}
$var = trim($args[1]);
self::exec($fd, 'self::evalCode', [$var]);
break;
case 'p':
case 'print':
$var = trim($args[1]);
self::exec($fd, 'self::printVariant', [$var]);
break;
case 'h':
case 'help':
self::output($fd, implode("\r\n", self::$menu));
break;
case 's':
case 'stats':
$stats = $serv->stats();
self::output($fd, var_export($stats, true));
break;
case 'c':
case 'coros':
self::exec($fd, 'self::getCoros', []);
break;
/**
* 查看协程堆栈
*/
case 'bt':
case 'b':
case 'backtrace':
if (empty($args[1])) {
self::output($fd, "invalid command [" . trim($args[1]) . "].");
break;
}
$_cid = intval($args[1]);
self::exec($fd, 'self::getBackTrace', [$_cid]);
break;
case 'i':
case 'info':
if (empty($args[1])) {
self::output($fd, "invalid command [" . trim($args[1]) . "].");
break;
}
$_fd = intval($args[1]);
$info = $serv->getClientInfo($_fd);
if (!$info) {
self::output($fd, "connection $_fd not found.");
} else {
self::output($fd, var_export($info, true));
}
break;
case 'l':
case 'list':
$tmp = array();
foreach ($serv->connections as $fd) {
$tmp[] = $fd;
if (count($tmp) > self::PAGESIZE) {
self::output($fd, json_encode($tmp));
$tmp = array();
}
}
if (count($tmp) > 0) {
self::output($fd, json_encode($tmp));
}
break;
case 'q':
case 'quit':
$serv->close($fd);
break;
default:
self::output($fd, "unknow command[$cmd]");
break;
}
}
}
function get_debug_print_backtrace($traces) {
$ret = array();
foreach ($traces as $i => $call) {
$object = '';
if (isset($call['class'])) {
$object = $call['class'] . $call['type'];
if (is_array($call['args'])) {
foreach ($call['args'] as &$arg) {
get_arg($arg);
}
}
}
$ret[] = '#' . str_pad($i, 3, ' ')
. $object . $call['function'] . '(' . implode(', ', $call['args'])
. ') called at [' . $call['file'] . ':' . $call['line'] . ']';
}
return implode("\n", $ret);
}
function get_arg(&$arg) {
if (is_object($arg)) {
$arr = (array)$arg;
$args = array();
foreach ($arr as $key => $value) {
if (strpos($key, chr(0)) !== false) {
$key = ''; // Private variable found
}
$args[] = '[' . $key . '] => ' . get_arg($value);
}
$arg = get_class($arg) . ' Object (' . implode(',', $args) . ')';
}
}

View File

@@ -1,84 +0,0 @@
<?php
namespace Framework;
use Co;
use Doctrine\Common\Annotations\AnnotationException;
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\WebSocket\Frame;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnEvent;
use ZM\Connection\ConnectionManager;
use ZM\Event\EventHandler;
use ZM\Http\Response;
class ServerEventHandler
{
/**
* @OnEvent("WorkerStart")
* @param Server $server
* @param $worker_id
* @throws AnnotationException
* @throws \ReflectionException
*/
public function onWorkerStart(Server $server, $worker_id) {
if ($server->taskworker === false) {
FrameworkLoader::$run_time = microtime(true);
EventHandler::callSwooleEvent("WorkerStart", $server, $worker_id);
} else {
ob_start();
AnnotationParser::registerMods();
//加载Custom目录下的自定义的内部类
ConnectionManager::registerCustomClass();
ob_get_clean();
}
}
/**
* @OnEvent("message")
* @param $server
* @param Frame $frame
* @throws AnnotationException
*/
public function onMessage($server, Frame $frame) {
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd);
EventHandler::callSwooleEvent("message", $server, $frame);
}
/**
* @OnEvent("request")
* @param $request
* @param $response
* @throws AnnotationException
*/
public function onRequest($request, $response) {
$response = new Response($response);
Console::debug("Receiving Http request event, cid=" . Co::getCid());
EventHandler::callSwooleEvent("request", $request, $response);
}
/**
* @OnEvent("open")
* @param $server
* @param Request $request
* @throws AnnotationException
*/
public function onOpen($server, Request $request) {
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
EventHandler::callSwooleEvent("open", $server, $request);
}
/**
* @OnEvent("close")
* @param $server
* @param $fd
* @throws AnnotationException
*/
public function onClose($server, $fd) {
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
EventHandler::callSwooleEvent("close", $server, $fd);
}
}

View File

@@ -1,121 +0,0 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2018/2/25
* Time: 下午11:11
*/
namespace Framework;
use Swoole\Atomic;
use Swoole\Database\PDOPool;
use swoole_atomic;
use ZM\connection\WSConnection;
class ZMBuf
{
//读写的缓存数据需要在worker_num = 1下才能正常使用
/** @var mixed[] ZMBuf的data */
private static $cache = [];
/** @var WSConnection[] */
static $connect = [];//储存连接实例的数组
//Scheduler计划任务连接实例只可以在单worker_num时使用
static $scheduler = null; //This is stupid warning...
//Swoole SQL连接池多进程下每个进程一个连接池
/** @var PDOPool */
static $sql_pool = null;//保存sql连接池的类
//只读的数据可以在多worker_num下使用
/** @var null|\Framework\GlobalConfig */
static $globals = null;
// swoole server操作对象每个进程均分配
/** @var \swoole_websocket_server $server */
static $server;
/** @var array Http请求uri路径根节点 */
public static $req_mapping_node;
/** @var mixed TimeNLP初始化后的对象每个进程均可初始化 */
public static $time_nlp;
/** @var string[] $custom_connection_class */
public static $custom_connection_class = [];//保存自定义的ws connection连接类型的
// Atomic可跨进程读写的原子计数任何地方均可使用
/** @var null|swoole_atomic */
static $info_level = null;//保存log等级的原子计数
public static $events = [];
/** @var Atomic[] */
public static $atomics;
public static $req_mapping = [];
public static $config = [];
public static $context = [];
public static $instance = [];
public static $context_class = [];
public static $server_events = [];
static function get($name, $default = null) {
return self::$cache[$name] ?? $default;
}
static function set($name, $value) {
self::$cache[$name] = $value;
}
static function append($name, $value) {
self::$cache[$name][] = $value;
}
static function appendKey($name, $key, $value) {
self::$cache[$name][$key] = $value;
}
static function appendKeyInKey($name, $key, $value) {
self::$cache[$name][$key][] = $value;
}
static function unsetCache($name) {
unset(self::$cache[$name]);
}
static function unsetByValue($name, $vale) {
$key = array_search($vale, self::$cache[$name]);
array_splice(self::$cache[$name], $key, 1);
}
static function isset($name) {
return isset(self::$cache[$name]);
}
static function array_key_exists($name, $key) {
return isset(self::$cache[$name][$key]);
}
static function in_array($name, $val) {
return in_array($val, self::$cache[$name]);
}
static function globals($key) {
return self::$globals->get($key);
}
static function config($config_name) {
return self::$config[$config_name] ?? null;
}
public static function resetCache() {
self::$cache = [];
self::$connect = [];
self::$time_nlp = null;
self::$instance = [];
}
/**
* 初始化atomic计数器
*/
public static function initAtomic() {
foreach (ZMBuf::globals("init_atomics") as $k => $v) {
self::$atomics[$k] = new Atomic($v);
}
}
}

View File

@@ -1,33 +0,0 @@
<?php
use Swoole\Coroutine\Http\Client;
Co\run(function (){
hello:
global $terminal_id, $port;
$client = new Client("127.0.0.1", $port);
$client->set(['websocket_mask' => true]);
$client->setHeaders(["x-terminal-id" => $terminal_id, 'x-pid' => posix_getppid()]);
$ret = $client->upgrade("/?type=terminal");
if ($ret) {
while (true) {
$line = fgets(STDIN);
if ($line !== false) {
$r = $client->push(trim($line));
if (trim($line) == "reload" || trim($line) == "r" || trim($line) == "stop") {
break;
}
if($r === false) {
echo "Unable to connect framework terminal, connection closed. Trying to reconnect after 5s.\n";
sleep(5);
goto hello;
}
} else {
break;
}
}
} else {
echo "Unable to connect framework terminal. port: $port\n";
}
});

View File

@@ -1,44 +1,66 @@
<?php
namespace Module\Example;
use Framework\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\SwooleEventAt;
use ZM\Connection\CQConnection;
use ZM\Event\EventDispatcher;
use ZM\Store\Redis\ZMRedis;
use ZM\Utils\ZMUtil;
/**
* Class Hello
* @package Module\Example
* @since 1.0
* @since 2.0
*/
class Hello
{
/**
* 在机器人连接后向终端输出信息
* @SwooleEventAt("open",rule="connectType:qq")
* @param $conn
* 一个简单的redis连接池使用demo将下方user_id改为你自己的QQ号即可(为了不被不法分子利用)
* @CQCommand("redis_test",user_id=627577391)
*/
public function onConnect(CQConnection $conn) {
Console::info("机器人 " . $conn->getQQ() . " 已连接!");
public function testCase() {
$a = new ZMRedis();
$redis = $a->get();
$r1 = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想设置的操作[r/w]");
switch ($r1) {
case "r":
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想读取的键名");
$result = $redis->get($k);
ctx()->reply("结果:" . $result);
break;
case "w":
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的键名");
$v = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的字符串");
$result = $redis->set($k, $v);
ctx()->reply("结果:" . ($result ? "成功" : "失败"));
break;
}
}
/**
* 在机器人连接后向终端输出信息
* @SwooleEventAt("close",rule="connectType:qq")
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
* @CQCommand(".reload",user_id=627577391)
*/
public function onDisconnect() {
$conn = ctx()->getConnection();
Console::info("机器人 " . $conn->getQQ() . " 已断开连接!");
public function reload() {
ctx()->reply("重启中...");
ZMUtil::reload();
}
/**
* 向机器人发送"你好",即可回复这句话
* @CQCommand("我是谁")
*/
public function whoami() {
$user = ctx()->getRobot()->getLoginInfo();
return "你是" . $user["data"]["nickname"] . "QQ号是" . $user["data"]["user_id"];
}
/**
* 向机器人发送"你好啊",也可回复这句话
* @CQCommand(match="你好",alias={"你好啊","你是谁"})
*/
public function hello() {
@@ -46,27 +68,22 @@ class Hello
}
/**
* @CQCommand(".reload")
*/
public function reload() {
context()->reply("reloading...");
ZMUtil::reload();
}
/**
* 一个简单随机数的功能demo
* 问法1随机数 1 20
* 问法2从1到20的随机数
* @CQCommand("随机数")
* @CQCommand(regexMatch="*从*到*的随机数")
* @param $arg
* @CQCommand(pattern="*从*到*的随机数")
* @return string
*/
public function randNum($arg) {
public function randNum() {
// 获取第一个数字类型的参数
$num1 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第一个数字");
$num1 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第一个数字");
// 获取第二个数字类型的参数
$num2 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第二个数字");
$num2 = ctx()->getArgs(ZM_MATCH_NUMBER, "请输入第二个数字");
$a = min(intval($num1), intval($num2));
$b = max(intval($num1), intval($num2));
// 回复用户结果
context()->reply("随机数是:".mt_rand($a, $b));
return "随机数是:" . mt_rand($a, $b);
}
/**
@@ -87,13 +104,48 @@ class Hello
return "Hello Zhamao!";
}
/**
* 使用自定义参数的路由参数
* @RequestMapping("/whoami/{name}")
* @param $param
* @return string
*/
public function paramGet($param) {
return "Your name: {$param["name"]}";
}
/**
* 在机器人连接后向终端输出信息
* @OnSwooleEvent("open",rule="connectIsQQ()")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
}
/**
* 在机器人断开连接后向终端输出信息
* @OnSwooleEvent("close",rule="connectIsQQ()")
* @param ConnectionObject $conn
*/
public function onDisconnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已断开连接!");
}
/**
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
* @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
*/
public function onRequest() {
EventDispatcher::interrupt();
}
/**
* 框架会默认关闭未知的WebSocket链接因为这个绑定的事件你可以根据你自己的需求进行修改
* @SwooleEventAt(type="open",rule="connectType:unknown")
* @OnSwooleEvent(type="open",rule="connectIsDefault()")
*/
public function closeUnknownConn() {
Console::info("Unknown connection , I will close it.");
context()->getConnection()->close();
server()->close(ctx()->getConnection()->getFd());
}
}

View File

@@ -2,24 +2,25 @@
namespace Module\Middleware;
use Framework\Console;
use ZM\Annotation\Http\After;
use ZM\Annotation\Http\Before;
use ZM\Annotation\Http\HandleAfter;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\HandleException;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Console\Console;
use ZM\Http\MiddlewareInterface;
/**
* Class AuthMiddleware
* Class TimerMiddleware
* 示例中间件:用于统计路由函数运行时间用的
* @package Module\Middleware
* @MiddlewareClass()
* @MiddlewareClass("timer")
*/
class TimerMiddleware implements MiddlewareInterface
{
private $starttime;
/**
* @Before()
* @HandleBefore()
* @return bool
*/
public function onBefore() {
@@ -28,11 +29,16 @@ class TimerMiddleware implements MiddlewareInterface
}
/**
* @After()
* @HandleAfter()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
}
public function getName() { return "timer"; }
/**
* @HandleException(\Exception::class)
*/
public function onException() {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
}
}

View File

@@ -1,29 +0,0 @@
<?php
namespace Scheduler;
use Swoole\Coroutine\Http\Client;
use Swoole\WebSocket\Frame;
class MessageEvent
{
/**
* @var Frame
*/
private $frame;
/**
* @var Client
*/
private $client;
public function __construct(Client $client, Frame $frame) {
$this->client = $client;
$this->frame = $frame;
}
public function onActivate() {
//TODO: 写Scheduler计时器内的处理逻辑
}
}

View File

@@ -1,140 +0,0 @@
<?php
namespace Scheduler;
use Exception;
use Framework\Console;
use Framework\GlobalConfig;
use Swoole\Coroutine;
use Swoole\Coroutine\Http\Client;
use Swoole\Process;
use Swoole\WebSocket\Frame;
class Scheduler
{
const PROCESS = 1;
const REMOTE = 2;
/**
* @var Process
*/
private $process = null;
/**
* @var int
*/
private $m_pid;
private $pid;
/**
* @var Scheduler
*/
private static $instance;
/**
* @var GlobalConfig
*/
private $settings;
/**
* @var Client
*/
private $client;
public function __construct($method = self::PROCESS, $option = []) {
if (self::$instance !== null) die("Cannot run two scheduler in on process!");
self::$instance = $this;
if ($method == self::PROCESS) $this->initProcess();
elseif ($method == self::REMOTE) $this->initRemote();
}
private function initProcess() { //TODO: 完成Process模式的代码
$m_pid = posix_getpid();
$this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true);
$this->pid = $this->process->start();
while (1) {
$ret = Process::wait();
if ($ret) {
$this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true);
$this->pid = $this->process->start();
echo "Reboot done.\n";
}
}
}
private function initRemote() {
define('WORKING_DIR', __DIR__ . '../..');
$this->requireGlobalFunctions();
$this->registerAutoloader('classLoader');
$this->settings = new GlobalConfig();
if (!$this->settings->success) die("Failed to load global config. Please check config/global.php file");
$this->defineProperties();
//start swoole Framework
$this->selfCheck();
try {
$host = $this->settings->get("scheduler")["host"];
$port = $this->settings->get("scheduler")["port"];
$token = $this->settings->get("scheduler")["token"];
$this->client = new Client($host, $port);
$path = "/" . ($token != "" ? ("?token=" . urlencode($token)) : "");
while (true) {
if ($this->client->upgrade($path)) {
while (true) {
$recv = $this->client->recv();
if ($recv instanceof Frame) {
(new MessageEvent($this->client, $recv))->onActivate();
} else {
break;
}
}
} else {
Console::warning("无法连接Framework将在5秒后重连...");
Coroutine::sleep(5);
}
}
} catch (Exception $e) {
Console::error($e);
}
}
private function requireGlobalFunctions() {
/** @noinspection PhpIncludeInspection */
require WORKING_DIR . '/src/Framework/global_functions.php';
}
private function registerAutoloader(string $string) {
if (!spl_autoload_register($string)) die("Failed to register autoloader named \"$string\" !");
}
private function defineProperties() {
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", $this->settings->get("zm_data"));
//define("CONFIG_DIR", $this->settings->get("config_dir"));
define("CRASH_DIR", $this->settings->get("crash_dir"));
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n");
if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\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");
return true;
}
private static function onWork(Process $worker, $m_pid) {
swoole_set_process_name('php-scheduler');
for ($j = 0; $j < 16000; $j++) {
self::checkMpid($worker, $m_pid);
echo "msg: {$j}\n";
sleep(1);
}
}
private static function checkMpid(Process $worker, $m_pid) {
if (!Process::kill($m_pid, 0)) {
$worker->exit(); //主进程死了我也死
// 这句提示,实际是看不到的.需要写到日志中
echo "Master process exited, I [{$worker['pid']}] also quit\n";
}
}
}

View File

@@ -4,8 +4,7 @@
namespace ZM\API;
use Framework\Console;
use ZM\Utils\ZMUtil;
use ZM\Console\Console;
class CQ
{

View File

@@ -3,239 +3,52 @@
namespace ZM\API;
use Co;
use Framework\Console;
use Framework\ZMBuf;
use ZM\Connection\ConnectionManager;
use ZM\Connection\CQConnection;
use ZM\Event\EventHandler;
use ZM\Utils\ZMRobot;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
/**
* @method static send_private_msg($self_id, $params, $function = null)
* @method static send_group_msg($self_id, $params, $function = null)
* @method static send_discuss_msg($self_id, $params, $function = null)
* @method static send_msg($self_id, $params, $function = null)
* @method static delete_msg($self_id, $params, $function = null)
* @method static send_like($self_id, $params, $function = null)
* @method static set_group_kick($self_id, $params, $function = null)
* @method static set_group_ban($self_id, $params, $function = null)
* @method static set_group_anonymous_ban($self_id, $params, $function = null)
* @method static set_group_whole_ban($self_id, $params, $function = null)
* @method static set_group_admin($self_id, $params, $function = null)
* @method static set_group_anonymous($self_id, $params, $function = null)
* @method static set_group_card($self_id, $params, $function = null)
* @method static set_group_leave($self_id, $params, $function = null)
* @method static set_group_special_title($self_id, $params, $function = null)
* @method static set_discuss_leave($self_id, $params, $function = null)
* @method static set_friend_add_request($self_id, $params, $function = null)
* @method static set_group_add_request($self_id, $params, $function = null)
* @method static get_login_info($self_id, $params, $function = null)
* @method static get_stranger_info($self_id, $params, $function = null)
* @method static get_group_list($self_id, $params, $function = null)
* @method static get_group_member_info($self_id, $params, $function = null)
* @method static get_group_member_list($self_id, $params, $function = null)
* @method static get_cookies($self_id, $params, $function = null)
* @method static get_csrf_token($self_id, $params, $function = null)
* @method static get_credentials($self_id, $params, $function = null)
* @method static get_record($self_id, $params, $function = null)
* @method static get_status($self_id, $params, $function = null)
* @method static get_version_info($self_id, $params, $function = null)
* @method static set_restart($self_id, $params, $function = null)
* @method static set_restart_plugin($self_id, $params, $function = null)
* @method static clean_data_dir($self_id, $params, $function = null)
* @method static clean_plugin_log($self_id, $params, $function = null)
* @method static _get_friend_list($self_id, $params, $function = null)
* @method static _get_group_info($self_id, $params, $function = null)
* @method static _get_vip_info($self_id, $params, $function = null)
* @method static send_private_msg_async($self_id, $params, $function = null)
* @method static send_group_msg_async($self_id, $params, $function = null)
* @method static send_discuss_msg_async($self_id, $params, $function = null)
* @method static send_msg_async($self_id, $params, $function = null)
* @method static delete_msg_async($self_id, $params, $function = null)
* @method static set_group_kick_async($self_id, $params, $function = null)
* @method static set_group_ban_async($self_id, $params, $function = null)
* @method static set_group_anonymous_ban_async($self_id, $params, $function = null)
* @method static set_group_whole_ban_async($self_id, $params, $function = null)
* @method static set_group_admin_async($self_id, $params, $function = null)
* @method static set_group_anonymous_async($self_id, $params, $function = null)
* @method static set_group_card_async($self_id, $params, $function = null)
* @method static set_group_leave_async($self_id, $params, $function = null)
* @method static set_group_special_title_async($self_id, $params, $function = null)
* @method static set_discuss_leave_async($self_id, $params, $function = null)
* @method static set_friend_add_request_async($self_id, $params, $function = null)
* @method static set_group_add_request_async($self_id, $params, $function = null)
*/
class CQAPI
trait CQAPI
{
public static function quick_reply(CQConnection $conn, $data, $msg, $yield = null) {
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);
case "discuss":
return (new ZMRobot($conn))->setCallback($yield)->sendDiscussMsg($data["discuss_id"], $msg);
}
return null;
}
/**
* @param $name
* @param $arg
* @return bool
* @deprecated
*/
public static function __callStatic($name, $arg) {
trigger_error("This dynamic CQAPI calling method will be removed after 2.0 version.", E_USER_DEPRECATED);
$all = self::getSupportedAPIs();
$find = null;
if (in_array($name, $all)) $find = $name;
else {
foreach ($all as $v) {
if (strtolower($name) == strtolower(str_replace("_", "", $v))) {
$find = $v;
break;
}
}
}
if ($find === null) {
Console::warning("Unknown API " . $name);
return false;
}
$reply = ["action" => $find];
if (!is_array($arg[1])) {
Console::warning("Error when parsing params. Please make sure your params is an array.");
return false;
}
if ($arg[1] != []) {
$reply["params"] = $arg[1];
}
if (!($arg[0] instanceof CQConnection)) {
$robot = ConnectionManager::getByType("qq", ["self_id" => $arg[0]]);
if ($robot == []) {
Console::warning("发送错误,机器人连接不存在!");
return false;
}
$arg[0] = $robot[0];
}
return self::processAPI($arg[0], $reply, $arg[2] ?? null);
}
/********************** non-API Part **********************/
private static function getSupportedAPIs() {
return [
"send_private_msg",
"send_group_msg",
"send_discuss_msg",
"send_msg",
"delete_msg",
"send_like",
"set_group_kick",
"set_group_ban",
"set_group_anonymous_ban",
"set_group_whole_ban",
"set_group_admin",
"set_group_anonymous",
"set_group_card",
"set_group_leave",
"set_group_special_title",
"set_discuss_leave",
"set_friend_add_request",
"set_group_add_request",
"get_login_info",
"get_stranger_info",
"get_group_list",
"get_group_member_info",
"get_group_member_list",
"get_cookies",
"get_csrf_token",
"get_credentials",
"get_record",
"get_status",
"get_version_info",
"set_restart",
"set_restart_plugin",
"clean_data_dir",
"clean_plugin_log",
"_get_friend_list",
"_get_group_info",
"_get_vip_info",
//异步API
"send_private_msg_async",
"send_group_msg_async",
"send_discuss_msg_async",
"send_msg_async",
"delete_msg_async",
"set_group_kick_async",
"set_group_ban_async",
"set_group_anonymous_ban_async",
"set_group_whole_ban_async",
"set_group_admin_async",
"set_group_anonymous_async",
"set_group_card_async",
"set_group_leave_async",
"set_group_special_title_async",
"set_discuss_leave_async",
"set_friend_add_request_async",
"set_group_add_request_async"
];
}
public static function getLoggedAPIs() {
return [
"send_private_msg",
"send_group_msg",
"send_discuss_msg",
"send_msg",
"send_private_msg_async",
"send_group_msg_async",
"send_discuss_msg_async",
"send_msg_async"
];
}
/**
* @param CQConnection $connection
* @param ConnectionObject $connection
* @param $reply
* @param |null $function
* @return bool|array
*/
public static function processAPI($connection, $reply, $function = null) {
$api_id = ZMBuf::$atomics["wait_msg_id"]->get();
private function processAPI($connection, $reply, $function = null) {
if ($connection->getOption("type") === CONN_WEBSOCKET)
return $this->processWebsocketAPI($connection, $reply, $function);
else
return $this->processHttpAPI($connection, $reply, $function);
}
public function processWebsocketAPI($connection, $reply, $function = false) {
$api_id = ZMAtomic::get("wait_msg_id")->add(1);
$reply["echo"] = $api_id;
ZMBuf::$atomics["wait_msg_id"]->add(1);
EventHandler::callCQAPISend($reply, $connection);
if (is_callable($function)) {
ZMBuf::appendKey("sent_api", $api_id, [
"data" => $reply,
"time" => microtime(true),
"func" => $function,
"self_id" => $connection->getQQ()
]);
} elseif ($function === true) {
ZMBuf::appendKey("sent_api", $api_id, [
"data" => $reply,
"time" => microtime(true),
"coroutine" => Co::getuid(),
"self_id" => $connection->getQQ()
]);
} else {
ZMBuf::appendKey("sent_api", $api_id, [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getQQ()
]);
}
if ($connection->push(json_encode($reply))) {
//Console::msg($reply, $connection->getQQ());
ZMBuf::$atomics["out_count"]->add(1);
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
$r[$api_id] = [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getOption("connect_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();
$data = ZMBuf::get("sent_api")[$api_id];
ZMBuf::unsetByValue("sent_api", $reply["echo"]);
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;
@@ -245,14 +58,30 @@ class CQAPI
"status" => "failed",
"retcode" => -1000,
"data" => null,
"self_id" => $connection->getQQ()
"self_id" => $connection->getOption("connect_id")
];
$s = ZMBuf::get("sent_api")[$reply["echo"]];
if (($s["func"] ?? null) !== null)
call_user_func($s["func"], $response, $reply);
ZMBuf::unsetByValue("sent_api", $reply["echo"]);
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
unset($r[$reply["echo"]]);
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
if ($function === true) return $response;
return false;
}
}
/**
* @param $connection
* @param $reply
* @param null $function
* @return bool
* @noinspection PhpUnusedParameterInspection
*/
public function processHttpAPI($connection, $reply, $function = null) {
return false;
}
public function __call($name, $arguments) {
return false;
}
}

689
src/ZM/API/ZMRobot.php Normal file
View File

@@ -0,0 +1,689 @@
<?php /** @noinspection PhpUnused */
namespace ZM\API;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Exception\RobotNotFoundException;
/**
* Class ZMRobot
* @package ZM\Utils
* @since 1.2
* @version V11
*/
class ZMRobot
{
use CQAPI;
const API_ASYNC = 1;
const API_NORMAL = 0;
const API_RATE_LIMITED = 2;
/** @var ConnectionObject|null */
private $connection;
private $callback = true;
private $prefix = 0;
/**
* @param $robot_id
* @return ZMRobot
* @throws RobotNotFoundException
*/
public static function get($robot_id) {
$r = ManagerGM::getAllByName('qq');
foreach ($r as $v) {
if ($v->getOption('connect_id') == $robot_id) return new ZMRobot($v);
}
throw new RobotNotFoundException("机器人 " . $robot_id . " 未连接到框架!");
}
/**
* @return ZMRobot
* @throws RobotNotFoundException
*/
public static function getRandom() {
$r = ManagerGM::getAllByName('qq');
if ($r == []) throw new RobotNotFoundException("没有任何机器人连接到框架!");
return new ZMRobot($r[array_rand($r)]);
}
public static function getFirst() {
}
/**
* @return ZMRobot[]
*/
public static function getAllRobot() {
$r = ManagerGM::getAllByName('qq');
$obj = [];
foreach($r as $v) {
$obj[] = new ZMRobot($v);
}
return $obj;
}
public function __construct(ConnectionObject $connection) {
$this->connection = $connection;
}
public function setCallback($callback = true) {
$this->callback = $callback;
return $this;
}
public function setPrefix($prefix = self::API_NORMAL) {
$this->prefix = $prefix;
return $this;
}
public function getSelfId() {
return $this->connection->getOption('connect_id');
}
/* 下面是 OneBot 标准的 V11 公开 API */
/**
* 发送私聊消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
* @param $user_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendPrivateMsg($user_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 发送群消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
* @param $group_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendGroupMsg($group_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 发送消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
* @param $message_type
* @param $target_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendMsg($message_type, $target_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_type' => $message_type,
($message_type == 'private' ? 'user' : $message_type) . '_id' => $target_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 撤回消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
* @param $message_id
* @return array|bool|null
*/
public function deleteMsg($message_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_id' => $message_id
]
], $this->callback);
}
/**
* 获取消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_msg-%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
* @param $message_id
* @return array|bool|null
*/
public function getMsg($message_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_id' => $message_id
]
], $this->callback);
}
/**
* 获取合并转发消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF
* @param $id
* @return array|bool|null
*/
public function getForwardMsg($id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'id' => $id
]
], $this->callback);
}
/**
* 发送好友赞
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_like-%E5%8F%91%E9%80%81%E5%A5%BD%E5%8F%8B%E8%B5%9E
* @param $user_id
* @param int $times
* @return array|bool|null
*/
public function sendLike($user_id, $times = 1) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'times' => $times
]
], $this->callback);
}
/**
* 群组踢人
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
* @param $group_id
* @param $user_id
* @param bool $reject_add_request
* @return array|bool|null
*/
public function setGroupKick($group_id, $user_id, $reject_add_request = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'reject_add_request' => $reject_add_request
]
], $this->callback);
}
/**
* 群组单人禁言
* @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
* @return array|bool|null
*/
public function setGroupBan($group_id, $user_id, $duration = 1800) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'duration' => $duration
]
], $this->callback);
}
/**
* 群组匿名用户禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous_ban-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D%E7%94%A8%E6%88%B7%E7%A6%81%E8%A8%80
* @param $group_id
* @param $anonymous_or_flag
* @param int $duration
* @return array|bool|null
*/
public function setGroupAnonymousBan($group_id, $anonymous_or_flag, $duration = 1800) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
(is_string($anonymous_or_flag) ? 'flag' : 'anonymous') => $anonymous_or_flag,
'duration' => $duration
]
], $this->callback);
}
/**
* 群组全员禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
* @param $group_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupWholeBan($group_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 群组设置管理员
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
* @param $group_id
* @param $user_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupAdmin($group_id, $user_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 群组匿名
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D
* @param $group_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupAnonymous($group_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 设置群名片(群备注)
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
* @param $group_id
* @param $user_id
* @param string $card
* @return array|bool|null
*/
public function setGroupCard($group_id, $user_id, $card = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'card' => $card
]
], $this->callback);
}
/**
* 设置群名
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
* @param $group_id
* @param $group_name
* @return array|bool|null
*/
public function setGroupName($group_id, $group_name) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'group_name' => $group_name
]
], $this->callback);
}
/**
* 退出群组
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
* @param $group_id
* @param bool $is_dismiss
* @return array|bool|null
*/
public function setGroupLeave($group_id, $is_dismiss = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'is_dismiss' => $is_dismiss
]
], $this->callback);
}
/**
* 设置群组专属头衔
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
* @param $group_id
* @param $user_id
* @param string $special_title
* @param int $duration
* @return array|bool|null
*/
public function setGroupSpecialTitle($group_id, $user_id, $special_title = "", $duration = -1) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'special_title' => $special_title,
'duration' => $duration
]
], $this->callback);
}
/**
* 处理加好友请求
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
* @param $flag
* @param bool $approve
* @param string $remark
* @return array|bool|null
*/
public function setFriendAddRequest($flag, $approve = true, $remark = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'flag' => $flag,
'approve' => $approve,
'remark' => $remark
]
], $this->callback);
}
/**
* 处理加群请求/邀请
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
* @param $flag
* @param $sub_type
* @param bool $approve
* @param string $reason
* @return array|bool|null
*/
public function setGroupAddRequest($flag, $sub_type, $approve = true, $reason = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'flag' => $flag,
'sub_type' => $sub_type,
'approve' => $approve,
'reason' => $reason
]
], $this->callback);
}
/**
* 获取登录号信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
* @return array|bool|null
*/
public function getLoginInfo() {
return $this->processAPI($this->connection, ['action' => $this->getActionName(__FUNCTION__)], $this->callback);
}
/**
* 获取陌生人信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
* @param $user_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getStrangerInfo($user_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取好友列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
* @return array|bool|null
*/
public function getFriendList() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取群信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
* @param $group_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getGroupInfo($group_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取群列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
* @return array|bool|null
*/
public function getGroupList() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取群成员信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $user_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getGroupMemberInfo($group_id, $user_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取群成员列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
* @param $group_id
* @return array|bool|null
*/
public function getGroupMemberList($group_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id
]
], $this->callback);
}
/**
* 获取群荣誉信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $type
* @return array|bool|null
*/
public function getGroupHonorInfo($group_id, $type) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'type' => $type
]
], $this->callback);
}
/**
* 获取 CSRF Token
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_csrf_token-%E8%8E%B7%E5%8F%96-csrf-token
* @return array|bool|null
*/
public function getCsrfToken() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取 QQ 相关接口凭证
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_credentials-%E8%8E%B7%E5%8F%96-qq-%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3%E5%87%AD%E8%AF%81
* @param string $domain
* @return array|bool|null
*/
public function getCredentials($domain = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'domain' => $domain
]
], $this->callback);
}
/**
* 获取语音
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_record-%E8%8E%B7%E5%8F%96%E8%AF%AD%E9%9F%B3
* @param $file
* @param $out_format
* @return array|bool|null
*/
public function getRecord($file, $out_format) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'file' => $file,
'out_format' => $out_format
]
], $this->callback);
}
/**
* 获取图片
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_image-%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87
* @param $file
* @return array|bool|null
*/
public function getImage($file) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'file' => $file
]
], $this->callback);
}
/**
* 检查是否可以发送图片
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
* @return array|bool|null
*/
public function canSendImage() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 检查是否可以发送语音
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
* @return array|bool|null
*/
public function canSendRecord() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取运行状态
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_status-%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81
* @return array|bool|null
*/
public function getStatus() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取版本信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
* @return array|bool|null
*/
public function getVersionInfo() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 重启 OneBot 实现
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
* @param int $delay
* @return array|bool|null
*/
public function setRestart($delay = 0) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'delay' => $delay
]
], $this->callback);
}
/**
* 清理缓存
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#clean_cache-%E6%B8%85%E7%90%86%E7%BC%93%E5%AD%98
* @return array|bool|null
*/
public function cleanCache() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
public function callExtendedAPI($action, $params = []) {
return $this->processAPI($this->connection, [
'action' => $action,
'params' => $params
], $this->callback);
}
private function getActionName(string $method) {
$prefix = ($this->prefix == self::API_ASYNC ? '_async' : ($this->prefix == self::API_RATE_LIMITED ? '_rate_limited' : ''));
$func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method));
return $func_name . $prefix;
}
}

View File

@@ -3,260 +3,187 @@
namespace ZM\Annotation;
use Doctrine\Common\Annotations\{AnnotationException, AnnotationReader};
use Co;
use Framework\{Console, ZMBuf};
use Error;
use Exception;
use Doctrine\Common\Annotations\AnnotationReader;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Console\Console;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ZM\Annotation\CQ\{CQAfter,
CQAPIResponse,
CQAPISend,
CQBefore,
CQCommand,
CQMessage,
CQMetaEvent,
CQNotice,
CQRequest
};
use ZM\Annotation\Http\{After, Before, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
use Swoole\Timer;
use ZM\Annotation\Interfaces\CustomAnnotation;
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Module\{Closed, InitBuffer, LoadBuffer, SaveBuffer};
use ZM\Annotation\Swoole\{OnSave, OnStart, OnTick, SwooleEventAfter, SwooleEventAt};
use ZM\Annotation\Interfaces\Rule;
use ZM\Connection\WSConnection;
use ZM\Event\EventHandler;
use ZM\Http\MiddlewareInterface;
use Framework\DataProvider;
use ZM\Utils\ZMUtil;
use ZM\Annotation\Module\Closed;
use ZM\Utils\DataProvider;
class AnnotationParser
{
private $path_list = [];
private $start_time;
private $annotation_map = [];
private $middleware_map = [];
private $middlewares = [];
/** @var null|AnnotationReader */
private $reader = null;
private $req_mapping = [];
/**
* 注册各个模块类的注解和模块level的排序
* @throws ReflectionException
* @throws AnnotationException
* AnnotationParser constructor.
*/
public static function registerMods() {
self::loadAnnotationClasses();
$all_class = getAllClasses(DataProvider::getWorkingDir() . "/src/Module/", "Module");
ZMBuf::$req_mapping[0] = [
public function __construct() {
$this->start_time = microtime(true);
$this->loadAnnotationClasses();
$this->req_mapping[0] = [
'id' => 0,
'pid' => -1,
'name' => '/'
];
$reader = new AnnotationReader();
foreach ($all_class as $v) {
Console::debug("正在检索 " . $v);
$reflection_class = new ReflectionClass($v);
$class_prefix = '';
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
$class_annotations = $reader->getClassAnnotations($reflection_class);
$middleware_addon = [];
foreach ($class_annotations as $vs) {
if ($vs instanceof Closed) {
continue 2;
} elseif ($vs instanceof Controller) {
Console::debug("找到 Controller 中间件: " . $vs->class);
$class_prefix = $vs->prefix;
} elseif ($vs instanceof SaveBuffer) {
Console::debug("注册自动保存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")");
DataProvider::addSaveBuffer($vs->buf_name, $vs->sub_folder);
} elseif ($vs instanceof LoadBuffer) {
Console::debug("注册到内存的缓存变量: " . $vs->buf_name . " (Dir:" . $vs->sub_folder . ")");
ZMBuf::set($vs->buf_name, DataProvider::getJsonData(($vs->sub_folder ?? "") . "/" . $vs->buf_name . ".json"));
} elseif ($vs instanceof InitBuffer) {
ZMBuf::set($vs->buf_name, []);
} elseif ($vs instanceof MiddlewareClass) {
Console::verbose("正在注册中间件 " . $reflection_class->getName());
$result = [
"class" => "\\" . $reflection_class->getName()
];
foreach ($methods as $vss) {
if ($vss->getName() == "getName") {
/** @var MiddlewareInterface $tmp */
$tmp = new $v();
$result["name"] = $tmp->getName();
continue;
}
$method_annotations = $reader->getMethodAnnotations($vss);
foreach ($method_annotations as $vsss) {
if ($vss instanceof Rule) $vss = self::registerRuleEvent($vsss, $vss, $reflection_class);
else $vss = self::registerMethod($vsss, $vss, $reflection_class);
//echo get_class($vsss) . PHP_EOL;
if ($vsss instanceof Before) $result["before"] = $vsss->method;
if ($vsss instanceof After) $result["after"] = $vsss->method;
if ($vsss instanceof HandleException) {
$result["exceptions"][$vsss->class_name] = $vsss->method;
}
}
}
ZMBuf::$events[MiddlewareClass::class][$result["name"]] = $result;
continue 2;
} elseif ($vs instanceof Middleware) {
$middleware_addon[] = $vs;
} elseif ($vs instanceof CustomAnnotation) {
$vs->class = $reflection_class->getName();
ZMBuf::$events[get_class($vs)][] = $vs;
}
}
foreach ($methods as $vs) {
if ($middleware_addon !== []) {
foreach($middleware_addon as $value){
Console::debug("Added middleware " . $value->middleware . " to $v -> " . $vs->getName());
ZMBuf::$events[MiddlewareInterface::class][$v][$vs->getName()][] = $value->middleware;
}
}
$method_annotations = $reader->getMethodAnnotations($vs);
foreach ($method_annotations as $vss) {
if ($vss instanceof Rule) $vss = self::registerRuleEvent($vss, $vs, $reflection_class);
else $vss = self::registerMethod($vss, $vs, $reflection_class);
Console::debug("寻找 " . $vs->getName() . " -> " . get_class($vss));
}
if ($vss instanceof SwooleEventAt) ZMBuf::$events[SwooleEventAt::class][] = $vss;
elseif ($vss instanceof SwooleEventAfter) ZMBuf::$events[SwooleEventAfter::class][] = $vss;
elseif ($vss instanceof CQMessage) ZMBuf::$events[CQMessage::class][] = $vss;
elseif ($vss instanceof CQNotice) ZMBuf::$events[CQNotice::class][] = $vss;
elseif ($vss instanceof CQRequest) ZMBuf::$events[CQRequest::class][] = $vss;
elseif ($vss instanceof CQMetaEvent) ZMBuf::$events[CQMetaEvent::class][] = $vss;
elseif ($vss instanceof CQCommand) ZMBuf::$events[CQCommand::class][] = $vss;
elseif ($vss instanceof RequestMapping) {
self::registerRequestMapping($vss, $vs, $reflection_class, $class_prefix);
} elseif ($vss instanceof CustomAnnotation) ZMBuf::$events[get_class($vss)][] = $vss;
elseif ($vss instanceof CQBefore) ZMBuf::$events[CQBefore::class][$vss->cq_event][] = $vss;
elseif ($vss instanceof CQAfter) ZMBuf::$events[CQAfter::class][$vss->cq_event][] = $vss;
elseif ($vss instanceof OnStart) ZMBuf::$events[OnStart::class][] = $vss;
elseif ($vss instanceof OnSave) ZMBuf::$events[OnSave::class][] = $vss;
elseif ($vss instanceof Middleware) ZMBuf::$events[MiddlewareInterface::class][$vss->class][$vss->method][] = $vss->middleware;
elseif ($vss instanceof OnTick) self::addTimerTick($vss);
elseif ($vss instanceof CQAPISend) ZMBuf::$events[CQAPISend::class][] = $vss;
elseif ($vss instanceof CQAPIResponse) ZMBuf::$events[CQAPIResponse::class][$vss->retcode] = [$vss->class, $vss->method];
/**
* 注册各个模块类的注解和模块level的排序
* @throws ReflectionException
*/
public function registerMods() {
foreach ($this->path_list as $path) {
Console::debug("parsing annotation in ".$path[0]);
$all_class = getAllClasses($path[0], $path[1]);
$this->reader = new AnnotationReader();
foreach ($all_class as $v) {
Console::debug("正在检索 " . $v);
$reflection_class = new ReflectionClass($v);
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
$class_annotations = $this->reader->getClassAnnotations($reflection_class);
// 这段为新加的:start
//这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析
/*
$annotation_map: {
Module\Example\Hello: {
class_annotations: [
注解对象1, 注解对象2, ...
],
methods: [
ReflectionMethod, ReflectionMethod, ...
],
methods_annotations: {
foo: [ 注解对象1, 注解对象2, ... ],
bar: [ 注解对象1, 注解对象2, ... ],
}
}
}
}
}
$tree = self::genTree(ZMBuf::$req_mapping);
ZMBuf::$req_mapping = $tree[0];
//给支持level的排个序
foreach (ZMBuf::$events as $class_name => $v) {
if (is_a($class_name, Level::class, true)) {
for ($i = 0; $i < count(ZMBuf::$events[$class_name]) - 1; ++$i) {
for ($j = 0; $j < count(ZMBuf::$events[$class_name]) - $i - 1; ++$j) {
$l1 = ZMBuf::$events[$class_name][$j]->level;
$l2 = ZMBuf::$events[$class_name][$j + 1]->level;
if ($l1 < $l2) {
$t = ZMBuf::$events[$class_name][$j + 1];
ZMBuf::$events[$class_name][$j + 1] = ZMBuf::$events[$class_name][$j];
ZMBuf::$events[$class_name][$j] = $t;
*/
// 生成主树
$this->annotation_map[$v]["class_annotations"] = $class_annotations;
$this->annotation_map[$v]["methods"] = $methods;
foreach ($methods as $method) {
$this->annotation_map[$v]["methods_annotations"][$method->getName()] = $this->reader->getMethodAnnotations($method);
}
foreach ($this->annotation_map[$v]["class_annotations"] as $ks => $vs) {
$vs->class = $v;
//预处理1将适用于每一个函数的注解到类注解重新注解到每个函数下面
if ($vs instanceof ErgodicAnnotation) {
foreach ($this->annotation_map[$v]["methods"] as $method) {
$copy = clone $vs;
$copy->method = $method->getName();
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
}
}
//预处理2处理 class 下面的注解
if ($vs instanceof Closed) {
unset($this->annotation_map[$v]);
continue 2;
} elseif ($vs instanceof MiddlewareClass) {
Console::verbose("正在注册中间件 " . $reflection_class->getName());
$rs = $this->registerMiddleware($vs, $reflection_class);
$this->middlewares[$rs["name"]] = $rs;
}
}
//预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach ($this->annotation_map[$v]["methods_annotations"] as $method_name => $methods_annotations) {
foreach ($methods_annotations as $method_anno) {
/** @var AnnotationBase $method_anno */
$method_anno->class = $v;
$method_anno->method = $method_name;
if ($method_anno instanceof RequestMapping) {
$this->registerRequestMapping($method_anno, $method_name, $v, $methods_annotations); //TODO: 用symfony的routing重写
} elseif ($method_anno instanceof Middleware) {
$this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno->middleware;
}
}
}
}
}
//预处理4生成路由树换成symfony后就不需要了
$tree = $this->genTree($this->req_mapping);
$this->req_mapping = $tree[0];
Console::debug("解析注解完毕!");
if (ZMBuf::isset("timer_count")) {
Console::info("Added " . ZMBuf::get("timer_count") . " timer(s)!");
ZMBuf::unsetCache("timer_count");
}
}
public static function getRuleCallback($rule_str) {
$func = null;
$rule = $rule_str;
if ($rule != "") {
$asp = explode(":", $rule);
$asp_name = array_shift($asp);
$rest = implode(":", $asp);
//Swoole 事件时走此switch
switch ($asp_name) {
case "connectType": //websocket连接类型
$func = function (?WSConnection $connection) use ($rest) {
if ($connection === null) return false;
return $connection->getType() == $rest ? true : false;
};
break;
case "containsGet": //handle http request事件时才能用
case "containsPost":
$get_list = explode(",", $rest);
if ($asp_name == "containsGet")
$func = function ($request) use ($get_list) {
foreach ($get_list as $v) if (!isset($request->get[$v])) return false;
return true;
};
else
$func = function ($request) use ($get_list) {
foreach ($get_list as $v) if (!isset($request->post[$v])) return false;
return true;
};
/*
if ($controller_prefix != '') {
$p = ZMBuf::$req_mapping_node;
$prefix_exp = explode("/", $controller_prefix);
foreach ($prefix_exp as $k => $v) {
if ($v == "" || $v == ".." || $v == ".") {
unset($prefix_exp[$k]);
}
}
while (($shift = array_shift($prefix_exp)) !== null) {
$p->addRoute($shift, new MappingNode($shift));
$p = $p->getRoute($shift);
}
if ($p->getNodeName() != "/") {
$p->setMethod($method->getName());
$p->setClass($class->getName());
$p->setRule($func);
return "mapped";
}
}*/
break;
case "containsJson": //handle http request事件时才能用
$json_list = explode(",", $rest);
$func = function ($json) use ($json_list) {
foreach ($json_list as $v) if (!isset($json[$v])) return false;
return true;
};
break;
case "dataEqual": //handle websocket message事件时才能用
$func = function ($data) use ($rest) {
return $data == $rest;
};
break;
/**
* @return array
*/
public function generateAnnotationEvents() {
$o = [];
foreach ($this->annotation_map as $module => $obj) {
foreach ($obj["class_annotations"] as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) continue;
else $o[get_class($class_annotation)][] = $class_annotation;
}
foreach ($obj["methods_annotations"] as $method_name => $methods_annotations) {
foreach ($methods_annotations as $annotation) {
$o[get_class($annotation)][] = $annotation;
}
}
switch ($asp_name) {
case "msgMatch": //handle cq message事件时才能用
$func = function ($msg) use ($rest) {
return matchPattern($rest, $msg);
};
break;
case "msgEqual": //handle cq message事件时才能用
$func = function ($msg) use ($rest) {
return trim($msg) == $rest;
};
break;
}
foreach ($o as $k => $v) {
$this->sortByLevel($o, $k);
}
return $o;
}
/**
* @return array
*/
public function getMiddlewares() { return $this->middlewares; }
/**
* @return array
*/
public function getMiddlewareMap() { return $this->middleware_map; }
/**
* @return array
*/
public function getReqMapping() { return $this->req_mapping; }
/**
* @param $path
* @param $indoor_name
*/
public function addRegisterPath($path, $indoor_name) { $this->path_list[] = [$path, $indoor_name]; }
//private function below
private function registerRequestMapping(RequestMapping $vss, $method, $class, $methods_annotations) {
$prefix = '';
foreach ($methods_annotations as $annotation) {
if ($annotation instanceof Controller) {
$prefix = $annotation->prefix;
break;
}
}
return $func;
}
public static function registerRuleEvent(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) {
$vss->callback = self::getRuleCallback($vss->getRule());
$vss->method = $method->getName();
$vss->class = $class->getName();
return $vss;
}
public static function registerMethod(?AnnotationBase $vss, ReflectionMethod $method, ReflectionClass $class) {
$vss->method = $method->getName();
$vss->class = $class->getName();
return $vss;
}
private static function registerRequestMapping(RequestMapping $vss, ReflectionMethod $method, ReflectionClass $class, string $prefix) {
$array = ZMBuf::$req_mapping;
$array = $this->req_mapping;
$uid = count($array);
$prefix_exp = explode("/", $prefix);
$route_exp = explode("/", $vss->route);
@@ -271,10 +198,11 @@ class AnnotationParser
}
}
if ($prefix_exp == [] && $route_exp == []) {
$array[0]['method'] = $method->getName();
$array[0]['class'] = $class->getName();
$array[0]['method'] = $method;
$array[0]['class'] = $class;
$array[0]['request_method'] = $vss->request_method;
ZMBuf::$req_mapping = $array;
$array[0]['route'] = $vss->route;
$this->req_mapping = $array;
return;
}
$pid = 0;
@@ -317,16 +245,19 @@ class AnnotationParser
];
$pid = $uid - 1;
}
$array[$uid - 1]['method'] = $method->getName();
$array[$uid - 1]['class'] = $class->getName();
$array[$uid - 1]['method'] = $method;
$array[$uid - 1]['class'] = $class;
$array[$uid - 1]['request_method'] = $vss->request_method;
ZMBuf::$req_mapping = $array;
$array[$uid - 1]['route'] = $vss->route;
$this->req_mapping = $array;
}
private static function loadAnnotationClasses() {
/** @noinspection PhpIncludeInspection */
private function loadAnnotationClasses() {
$class = getAllClasses(WORKING_DIR . "/src/ZM/Annotation/", "ZM\\Annotation");
foreach ($class as $v) {
$s = WORKING_DIR . '/src/' . str_replace("\\", "/", $v) . ".php";
//Console::debug("Requiring annotation " . $s);
require_once $s;
}
$class = getAllClasses(DataProvider::getWorkingDir() . "/src/Custom/Annotation/", "Custom\\Annotation");
@@ -337,7 +268,7 @@ class AnnotationParser
}
}
public static function genTree($items) {
private function genTree($items) {
$tree = array();
foreach ($items as $item)
if (isset($items[$item['pid']]))
@@ -347,44 +278,33 @@ class AnnotationParser
return $tree;
}
private static function addTimerTick(?OnTick $vss) {
ZMBuf::set("timer_count", ZMBuf::get("timer_count", 0) + 1);
$class = ZMUtil::getModInstance($vss->class);
$method = $vss->method;
$ms = $vss->tick_ms;
$cid = go(function () use ($class, $method, $ms) {
Co::suspend();
$plain_class = get_class($class);
if (!isset(ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method])) {
Console::debug("Added timer: " . $plain_class . " -> " . $method);
Timer::tick($ms, function () use ($class, $method) {
set_coroutine_params([]);
try {
$class->$method();
} catch (Exception $e) {
Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})");
} catch (Error $e) {
Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage());
echo Console::setColor($e->getTraceAsString(), "gray");
Console::error("Please check your code!");
}
});
} else {
Console::debug("Added Middleware-based timer: " . $plain_class . " -> " . $method);
Timer::tick($ms, function () use ($class, $method) {
set_coroutine_params([]);
try {
EventHandler::callWithMiddleware($class, $method, [], []);
} catch (Exception $e) {
Console::error("Uncaught error from TimerTick: " . $e->getMessage() . " at " . $e->getFile() . "({$e->getLine()})");
} catch (Error $e) {
Console::error("Uncaught fatal error from TimerTick: " . $e->getMessage());
echo Console::setColor($e->getTraceAsString(), "gray");
Console::error("Please check your code!");
}
});
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class) {
$result = [
"class" => "\\" . $reflection_class->getName(),
"name" => $vs->name
];
foreach ($reflection_class->getMethods() as $vss) {
$method_annotations = $this->reader->getMethodAnnotations($vss);
foreach ($method_annotations as $vsss) {
if ($vsss instanceof HandleBefore) $result["before"] = $vss->getName();
if ($vsss instanceof HandleAfter) $result["after"] = $vss->getName();
if ($vsss instanceof HandleException) {
$result["exceptions"][$vsss->class_name] = $vss->getName();
}
}
});
ZMBuf::append("paused_tick", $cid);
}
return $result;
}
private function sortByLevel(&$events, string $class_name, $prefix = "") {
if (is_a($class_name, Level::class, true)) {
$class_name .= $prefix;
usort($events[$class_name], function ($a, $b) {
$left = $a->level;
$right = $b->level;
return $left > $right ? -1 : ($left == $right ? 0 : 1);
});
}
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace ZM\Annotation\CQ;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
/**
* Class CQAPISend
* @package ZM\Annotation\CQ
* @Annotation
* @Target("METHOD")
*/
class CQAPISend extends AnnotationBase implements Level
{
/**
* @var string
*/
public $action = "";
/**
* @var bool
*/
public $with_result = false;
public $level = 20;
/**
* @return mixed
*/
public function getLevel() {
return $this->level;
}
/**
* @param mixed $level
*/
public function setLevel($level) {
$this->level = $level;
}
}

View File

@@ -18,9 +18,15 @@ class CQCommand extends AnnotationBase implements Level
/** @var string */
public $match = "";
/** @var string */
public $regexMatch = "";
public $pattern = "";
/** @var string */
public $fullMatch = "";
public $regex = "";
/** @var string */
public $start_with = "";
/** @var string */
public $end_with = "";
/** @var string */
public $keyword = "";
/** @var string[] */
public $alias = [];
/** @var string */

View File

@@ -6,6 +6,7 @@ namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
/**
* Class Controller
@@ -13,11 +14,11 @@ use ZM\Annotation\AnnotationBase;
* @Target("CLASS")
* @package ZM\Annotation\Http
*/
class Controller extends AnnotationBase
class Controller extends AnnotationBase implements ErgodicAnnotation
{
/**
* @var string
* @Required()
*/
public $prefix = '';
}
}

View File

@@ -9,11 +9,11 @@ use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class After
* Class HandleAfter
* @package ZM\Annotation\Http
* @Annotation
* @Target("METHOD")
*/
class After extends AnnotationBase
class HandleAfter extends AnnotationBase
{
}
}

View File

@@ -4,16 +4,15 @@
namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class Before
* Class HandleBefore
* @package ZM\Annotation\Http
* @Annotation
* @Target("METHOD")
*/
class Before extends AnnotationBase
class HandleBefore extends AnnotationBase
{
}
}

View File

@@ -7,6 +7,7 @@ namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
/**
* Class Middleware
@@ -14,7 +15,7 @@ use ZM\Annotation\AnnotationBase;
* @Annotation
* @Target("ALL")
*/
class Middleware extends AnnotationBase
class Middleware extends AnnotationBase implements ErgodicAnnotation
{
/**
* @var string

View File

@@ -4,6 +4,7 @@
namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
@@ -15,5 +16,9 @@ use ZM\Annotation\AnnotationBase;
*/
class MiddlewareClass extends AnnotationBase
{
}
/**
* @var string
* @Required()
*/
public $name = '';
}

View File

@@ -10,7 +10,7 @@ use ZM\Annotation\AnnotationBase;
/**
* Class RequestMapping
* @Annotation
* @Target("ALL")
* @Target("METHOD")
* @package ZM\Annotation\Http
*/
class RequestMapping extends AnnotationBase
@@ -36,4 +36,4 @@ class RequestMapping extends AnnotationBase
* @var array
*/
public $params = [];
}
}

View File

@@ -9,7 +9,6 @@ use ZM\Annotation\AnnotationBase;
/**
* Class RequestMethod
* @Annotation
*
* @package ZM\Annotation\Http
*/
class RequestMethod extends AnnotationBase
@@ -27,4 +26,4 @@ class RequestMethod extends AnnotationBase
public const DELETE = 'DELETE';
public const OPTIONS = 'OPTIONS';
public const HEAD = 'HEAD';
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace ZM\Annotation\Interfaces;
interface ErgodicAnnotation
{
}

View File

@@ -1,19 +0,0 @@
<?php
namespace ZM\Annotation\Module;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class InitBuffer
* @Annotation
* @Target("CLASS")
* @package ZM\Annotation\Module
*/
class InitBuffer
{
/** @var string @Required() */
public $buf_name;
}

View File

@@ -1,24 +0,0 @@
<?php
namespace ZM\Annotation\Module;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class LoadBuffer
* @package ZM\Annotation\Module
* @Annotation
* @Target("CLASS")
*/
class LoadBuffer
{
/**
* @var string
* @Required()
*/
public $buf_name;
/** @var string $sub_folder */
public $sub_folder = null;
}

View File

@@ -1,26 +0,0 @@
<?php
namespace ZM\Annotation\Module;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
/**
* Class SaveBuffer
* @Annotation
* @Target("CLASS")
* @package ZM\Annotation\Module
*/
class SaveBuffer
{
/**
* @var string
* @Required()
*/
public $buf_name;
/** @var string $sub_folder */
public $sub_folder = null;
}

View File

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

View File

@@ -7,12 +7,15 @@ use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class OnStart
* Class OnWorkerStart
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("ALL")
*/
class OnStart extends AnnotationBase
{
}
/**
* @var int
*/
public $worker_id = 0;
}

View File

@@ -10,12 +10,12 @@ use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Interfaces\Rule;
/**
* Class SwooleEventAt
* Class OnSwooleEvent
* @Annotation
* @Target("ALL")
* @package ZM\Annotation\Swoole
*/
class SwooleEventAt extends AnnotationBase implements Rule, Level
class OnSwooleEvent extends AnnotationBase implements Rule, Level
{
/**
* @var string
@@ -73,4 +73,4 @@ class SwooleEventAt extends AnnotationBase implements Rule, Level
$this->level = $level;
}
}
}

View File

@@ -1,15 +0,0 @@
<?php
namespace ZM\Annotation\Swoole;
/**
* Class OnTaskWorkerStart
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnTaskWorkerStart
{
}

View File

@@ -22,4 +22,9 @@ class OnTick extends AnnotationBase
* @Required()
*/
public $tick_ms;
/**
* @var int
*/
public $worker_id = 0;
}

View File

@@ -1,76 +0,0 @@
<?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\Level;
use ZM\Annotation\Interfaces\Rule;
/**
* Class SwooleEventAfter
* @Annotation
* @Target("ALL")
* @package ZM\Annotation\Swoole
*/
class SwooleEventAfter extends AnnotationBase implements Rule, Level
{
/**
* @var string
* @Required
*/
public $type;
/** @var string */
public $rule = "";
/** @var int */
public $level = 20;
/**
* @return string
*/
public function getType(): string {
return $this->type;
}
/**
* @param string $type
*/
public function setType(string $type) {
$this->type = $type;
}
/**
* @return string
*/
public function getRule(): string {
return $this->rule;
}
/**
* @param string $rule
*/
public function setRule(string $rule) {
$this->rule = $rule;
}
/**
* @return int
*/
public function getLevel(): int {
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
$this->level = $level;
}
}

View File

@@ -9,12 +9,12 @@ use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class OnEvent
* Class SwooleHandler
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("METHOD")
*/
class OnEvent extends AnnotationBase
class SwooleHandler extends AnnotationBase
{
/**
* @var string

View File

@@ -0,0 +1,73 @@
<?php
namespace ZM\Command;
use Phar;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Console\TermColor;
class BuildCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'build';
/**
* @var OutputInterface
*/
private $output = null;
protected function configure() {
$this->setDescription("Build an \".phar\" file | 将项目构建一个phar包");
$this->setHelp("此功能将会把炸毛框架的模块打包为\".phar\",供发布和执行。");
$this->addOption("target", "D", InputOption::VALUE_REQUIRED, "Output Directory | 指定输出目录");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
$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);
return Command::FAILURE;
}
$output->writeln("Target: " . $target_dir . " , Version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["version"]));
if (mb_substr($target_dir, -1, 1) !== '/') $target_dir .= "/";
if (ini_get('phar.readonly') == 1) {
$output->writeln(TermColor::color8(31) . "You need to set \"phar.readonly\" to \"Off\"!");
$output->writeln(TermColor::color8(31) . "See: https://stackoverflow.com/questions/34667606/cant-enable-phar-writing");
return Command::FAILURE;
}
if (!is_dir($target_dir)) {
$output->writeln(TermColor::color8(31) . "Error: No such file or directory ($target_dir)" . TermColor::RESET);
return Command::FAILURE;
}
$filename = "server.phar";
$this->build($target_dir, $filename);
return Command::SUCCESS;
}
private function build ($target_dir, $filename) {
@unlink($target_dir . $filename);
$phar = new Phar($target_dir . $filename);
$phar->startBuffering();
$src = realpath(__DIR__ . '/../../zhamao-framework/');
$hello = file_get_contents($src . '/src/Module/Example/Hello.php');
$middleware = file_get_contents($src . '/src/Module/Middleware/TimerMiddleware.php');
unlink($src . '/src/Module/Example/Hello.php');
unlink($src . '/src/Module/Middleware/TimerMiddleware.php');
$phar->buildFromDirectory($src);
$phar->addFromString('tmp/Hello.php.bak', $hello);
$phar->addFromString('tmp/TimerMiddleware.php.bak', $middleware);
//$phar->compressFiles(Phar::GZ);
$phar->setStub($phar->createDefaultStub('phar-starter.php'));
$phar->stopBuffering();
file_put_contents($src . '/src/Module/Example/Hello.php', $hello);
file_put_contents($src . '/src/Module/Middleware/TimerMiddleware.php', $middleware);
$this->output->writeln("Successfully built. Location: " . $target_dir . "$filename");
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace ZM\Command;
use Phar;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class InitCommand extends Command
{
private $extract_files = [
"/config/global.php",
"/.gitignore",
"/config/file_header.json",
"/config/console_color.json",
"/config/motd.txt",
"/src/Module/Example/Hello.php",
"/src/Module/Middleware/TimerMiddleware.php",
"/src/Custom/global_function.php"
];
// the name of the command (the part after "bin/console")
protected static $defaultName = 'init';
protected function configure() {
$this->setDescription("Initialize framework starter | 初始化框架运行的基础文件");
$this->setHelp("此命令将会解压以下文件到项目的根目录:\n" . implode("\n", $this->getExtractFiles()));
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
if (LOAD_MODE === 1) { // 从composer依赖而来的项目模式最基本的需要初始化的模式
$output->writeln("<comment>Initializing files</comment>");
$base_path = LOAD_MODE_COMPOSER_PATH;
foreach ($this->extract_files as $file) {
if (!file_exists($base_path . $file)) {
$info = pathinfo($file);
@mkdir($base_path . $info["dirname"], 0777, true);
echo "Copying " . $file . PHP_EOL;
$package_name = ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["name"]);
copy($base_path . "/vendor/" . $package_name . $file, $base_path . $file);
} else {
echo "Skipping " . $file . " , file exists." . PHP_EOL;
}
}
$autoload = [
"psr-4" => [
"Module\\" => "src/Module",
"Custom\\" => "src/Custom"
],
"files" => [
"src/Custom/global_function.php"
]
];
if (file_exists($base_path . "/composer.json")) {
$composer = json_decode(file_get_contents($base_path . "/composer.json"), true);
if (!isset($composer["autoload"])) {
$composer["autoload"] = $autoload;
} else {
foreach ($autoload["psr-4"] as $k => $v) {
if (!isset($composer["autoload"]["psr-4"][$k])) $composer["autoload"]["psr-4"][$k] = $v;
}
foreach ($autoload["files"] as $k => $v) {
if (!in_array($v, $composer["autoload"]["files"])) $composer["autoload"]["files"][] = $v;
}
}
file_put_contents($base_path . "/composer.json", json_encode($composer, 64 | 128 | 256));
$output->writeln("<info>Executing composer update command</info>");
exec("composer update");
echo PHP_EOL;
} else {
echo("Error occurred. Please check your updates.\n");
return Command::FAILURE;
}
return Command::SUCCESS;
} elseif (LOAD_MODE === 2) { //从phar启动的框架包初始化的模式
$phar_link = new Phar(__DIR__);
$current_dir = pathinfo($phar_link->getPath())["dirname"];
chdir($current_dir);
$phar_link = "phar://" . $phar_link->getPath();
foreach ($this->extract_files as $file) {
if (!file_exists($current_dir . $file)) {
$info = pathinfo($file);
@mkdir($current_dir . $info["dirname"], 0777, true);
echo "Copying " . $file . PHP_EOL;
file_put_contents($current_dir . $file, file_get_contents($phar_link . $file));
} else {
echo "Skipping " . $file . " , file exists." . PHP_EOL;
}
}
}
$output->writeln("initialization must be started with composer-project mode!");
return Command::FAILURE;
}
private function getExtractFiles() {
return $this->extract_files;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace ZM\Command;
use Swoole\Atomic;
use Swoole\Coroutine;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Http\Server;
use Swoole\Process;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\ZMAtomic;
use ZM\Utils\HttpUtil;
class PureHttpCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'simple-http-server';
protected function configure() {
$this->setDescription("Run a simple http server | 启动一个简单的文件 HTTP 服务器");
$this->setHelp("直接运行可以启动");
$this->addArgument('dir', InputArgument::REQUIRED, 'Your directory');
$this->addOption("host", 'H', InputOption::VALUE_REQUIRED, "启动监听地址");
$this->addOption("port", 'P', InputOption::VALUE_REQUIRED, "启动监听地址的端口");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
$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>");
return self::FAILURE;
}
$global = ZMConfig::get("global");
$host = $input->getOption("host") ?? $global["host"];
$port = $input->getOption("port") ?? $global["port"];
$server = new Server($host, $port);
$server->set(ZMConfig::get("global", "swoole"));
Console::init(0, $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(
$request->server["request_uri"],
$response,
[
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => $index
]);
echo "\r".Coroutine::stats()["coroutine_peak_num"];
});
$server->on("start", function ($server) {
Process::signal(SIGINT, function () use ($server) {
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";
}
$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))
return Command::SUCCESS;
// or return this if some error happened during the execution
// (it's equivalent to returning int(1))
// return Command::FAILURE;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Framework;
class RunServerCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'server';
protected function configure() {
$this->setDefinition([
new InputOption("debug-mode", "D", null, "开启调试模式 (这将关闭协程化)"),
new InputOption("log-debug", null, null, "调整消息等级到debug (log-level=4)"),
new InputOption("log-verbose", null, null, "调整消息等级到verbose (log-level=3)"),
new InputOption("log-info", null, null, "调整消息等级到info (log-level=2)"),
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-coroutine", null, null, "关闭协程Hook"),
new InputOption("daemon", null, null, "以守护进程的方式运行框架"),
new InputOption("watch", null, null, "监听 src/ 目录的文件变化并热更新"),
new InputOption("env", null, InputOption::VALUE_REQUIRED, "设置环境类型 (production, development, staging)"),
]);
$this->setDescription("Run zhamao-framework | 启动框架");
$this->setHelp("直接运行可以启动");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
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;
}
}
// ... put here the code to run in your command
// this method must return an integer number with the "exit status code"
// of the command. You can also use these constants to make code more readable
(new Framework($input->getOptions()))->start();
// return this if there was no problem running the command
// (it's equivalent to returning int(0))
return Command::SUCCESS;
// or return this if some error happened during the execution
// (it's equivalent to returning int(1))
// return Command::FAILURE;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
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) {
//TODO: 写一个生成systemd配置的功能给2.0
return Command::SUCCESS;
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace ZM\Connection;
class CQConnection extends WSConnection
{
public $self_id = null;
public function __construct($server, $fd, $self_id) {
parent::__construct($server, $fd);
$this->self_id = $self_id;
}
public function getQQ(){
return $this->self_id;
}
public function getType() {
return "qq";
}
}

View File

@@ -1,76 +0,0 @@
<?php
namespace ZM\Connection;
use Framework\ZMBuf;
use Framework\DataProvider;
class ConnectionManager
{
/**
* 通过server的fd获取WSConnection实例化对象
* @param int $fd
* @return WSConnection|CQConnection|ProxyConnection
*/
public static function get(int $fd) {
foreach (ZMBuf::$connect as $v) {
if ($v->fd == $fd) return $v;
}
return null;
}
/**
* @param string $type
* @param array $option
* @return WSConnection[]|CQConnection[]
*/
public static function getByType(string $type, $option = []) {
$conn = [];
foreach (ZMBuf::$connect as $v) {
foreach ($option as $ks => $vs) {
if (($v->$ks ?? "") == $vs) continue;
else continue 2;
}
if ($v->getType() == $type) $conn[] = $v;
}
return $conn;
}
public static function getTypeClassName(string $type) {
switch (strtolower($type)) {
case "qq":
case "universal":
return CQConnection::class;
case "webconsole":
return WCConnection::class;
case "proxy":
return ProxyConnection::class;
case "terminal":
return TerminalConnection::class;
default:
foreach (ZMBuf::$custom_connection_class as $v) {
/** @var WSConnection $r */
$r = new $v(ZMBuf::$server, -1);
if ($r->getType() == strtolower($type)) return $v;
}
return UnknownConnection::class;
}
}
public static function close($fd) {
foreach (ZMBuf::$connect as $k => $v) {
if ($v->fd == $fd) {
ZMBuf::$server->close($fd);
unset(ZMBuf::$connect[$k]);
break;
}
}
}
public static function registerCustomClass() {
$classes = getAllClasses(DataProvider::getWorkingDir(). "/src/Custom/Connection/", "Custom\\Connection");
ZMBuf::$custom_connection_class = $classes;
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace ZM\Connection;
class ProxyConnection extends WSConnection
{
public function getType() {
return "proxy";
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace ZM\Connection;
class TerminalConnection extends WSConnection
{
public function getType() {
return "terminal";
}
}

View File

@@ -1,13 +0,0 @@
<?php
namespace ZM\Connection;
class UnknownConnection extends WSConnection
{
public function getType() {
return "unknown";
}
}

View File

@@ -1,10 +0,0 @@
<?php
namespace ZM\Connection;
class WCConnection extends WSConnection
{
public function getType() { return "wc"; }
}

View File

@@ -1,51 +0,0 @@
<?php
namespace ZM\Connection;
use Framework\Console;
use swoole_websocket_server;
abstract class WSConnection
{
public $fd;
/** @var swoole_websocket_server */
protected $server;
public $available = false;
public function __construct($server, $fd) {
$this->server = $server;
$this->fd = $fd;
}
public abstract function getType();
public function exists() {
return $this->available = $this->server->exist($this->fd);
}
public function close() {
ConnectionManager::close($this->fd);
}
public function push($data, $push_error_record = true) {
if ($data === null || $data == "") {
Console::warning("推送了空消息");
return false;
}
if (!$this->server->exist($this->fd)) {
Console::warning("Swoole 原生 websocket连接池中无此连接");
return false;
}
if ($this->server->push($this->fd, $data) === false) {
$data = unicode_decode($data);
if ($push_error_record) Console::warning("API push failed. Data: " . $data);
Console::warning("websocket数据未成功推送长度" . strlen($data));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace ZM;
use Exception;
use ZM\Command\InitCommand;
use ZM\Command\PureHttpCommand;
use ZM\Command\RunServerCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
class ConsoleApplication extends Application
{
public function __construct(string $name = 'UNKNOWN') {
$version = json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "UNKNOWN";
parent::__construct($name, $version);
}
public function initEnv() {
$this->selfCheck();
//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";
echo "* This is repository mode.\n";
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";
$r = strtolower(trim(fgets(STDIN)));
if ($r === "" || $r === "y") {
$composer["autoload"]["psr-4"]["Module\\"] = "src/Module";
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) {
echo "成功添加!请重新进行 composer update \n";
exit(1);
} else {
echo "添加失败!请按任意键继续!";
fgets(STDIN);
exit(1);
}
} else {
exit(1);
}
}
}
$this->addCommands([
new RunServerCommand(), //运行主服务的指令控制器
new InitCommand(), //初始化用的用于项目初始化和phar初始化
new PureHttpCommand() //纯HTTP服务器指令
]);
/*
$command_register = ZMConfig::get("global", "command_register_class") ?? [];
foreach ($command_register as $v) {
$obj = new $v();
if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command");
$this->add($obj);
}*/
}
/**
* @param InputInterface|null $input
* @param OutputInterface|null $output
* @return int
*/
public function run(InputInterface $input = null, OutputInterface $output = null) {
try {
return parent::run($input, $output);
} catch (Exception $e) {
die("{$e->getMessage()} at {$e->getFile()}({$e->getLine()})");
}
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19");
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");
return true;
}
}

View File

@@ -5,21 +5,22 @@ namespace ZM\Context;
use Co;
use Framework\ZMBuf;
use Exception;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use swoole_server;
use ZM\API\CQAPI;
use ZM\Connection\ConnectionManager;
use ZM\Connection\CQConnection;
use ZM\Connection\WSConnection;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
use ZM\Exception\InvalidArgumentException;
use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response;
use ZM\Utils\ZMRobot;
use ZM\API\ZMRobot;
use ZM\Utils\CoMessage;
class Context implements ContextInterface
{
public static $context = [];
private $cid;
public function __construct($cid) { $this->cid = $cid; }
@@ -27,34 +28,34 @@ class Context implements ContextInterface
/**
* @return swoole_server|null
*/
public function getServer() { return ZMBuf::$context[$this->cid]["server"] ?? null; }
public function getServer() { return self::$context[$this->cid]["server"] ?? server(); }
/**
* @return Frame|null
*/
public function getFrame() { return ZMBuf::$context[$this->cid]["frame"] ?? null; }
public function getFrame() { return self::$context[$this->cid]["frame"] ?? null; }
public function getFd() { return ZMBuf::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; }
public function getFd() { return self::$context[$this->cid]["fd"] ?? $this->getFrame()->fd ?? null; }
/**
* @return array|null
*/
public function getData() { return ZMBuf::$context[$this->cid]["data"] ?? null; }
public function getData() { return self::$context[$this->cid]["data"] ?? null; }
public function setData($data) { ZMBuf::$context[$this->cid]["data"] = $data; }
public function setData($data) { self::$context[$this->cid]["data"] = $data; }
/**
* @return Request|null
*/
public function getRequest() { return ZMBuf::$context[$this->cid]["request"] ?? null; }
public function getRequest() { return self::$context[$this->cid]["request"] ?? null; }
/**
* @return Response|null
*/
public function getResponse() { return ZMBuf::$context[$this->cid]["response"] ?? null; }
public function getResponse() { return self::$context[$this->cid]["response"] ?? null; }
/** @return WSConnection */
public function getConnection() { return ConnectionManager::get($this->getFd()); }
/** @return ConnectionObject|null */
public function getConnection() { return ManagerGM::get($this->getFd()); }
/**
* @return int|null
@@ -65,37 +66,37 @@ class Context implements ContextInterface
* @return ZMRobot|null
*/
public function getRobot() {
$conn = ConnectionManager::get($this->getFrame()->fd);
return $conn instanceof CQConnection ? new ZMRobot($conn) : null;
$conn = ManagerGM::get($this->getFrame()->fd);
return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null;
}
public function getMessage() { return ZMBuf::$context[$this->cid]["data"]["message"] ?? null; }
public function getMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; }
public function setMessage($msg) { ZMBuf::$context[$this->cid]["data"]["message"] = $msg; }
public function setMessage($msg) { self::$context[$this->cid]["data"]["message"] = $msg; }
public function getUserId() { return $this->getData()["user_id"] ?? null; }
public function setUserId($id) { ZMBuf::$context[$this->cid]["data"]["user_id"] = $id; }
public function setUserId($id) { self::$context[$this->cid]["data"]["user_id"] = $id; }
public function getGroupId() { return $this->getData()["group_id"] ?? null; }
public function setGroupId($id) { ZMBuf::$context[$this->cid]["data"]["group_id"] = $id; }
public function setGroupId($id) { self::$context[$this->cid]["data"]["group_id"] = $id; }
public function getDiscussId() { return $this->getData()["discuss_id"] ?? null; }
public function setDiscussId($id) { ZMBuf::$context[$this->cid]["data"]["discuss_id"] = $id; }
public function setDiscussId($id) { self::$context[$this->cid]["data"]["discuss_id"] = $id; }
public function getMessageType() { return $this->getData()["message_type"] ?? null; }
public function setMessageType($type) { ZMBuf::$context[$this->cid]["data"]["message_type"] = $type; }
public function setMessageType($type) { self::$context[$this->cid]["data"]["message_type"] = $type; }
public function getRobotId() { return $this->getData()["self_id"] ?? null; }
public function getCache($key) { return ZMBuf::$context[$this->cid]["cache"][$key] ?? null; }
public function getCache($key) { return self::$context[$this->cid]["cache"][$key] ?? null; }
public function setCache($key, $value) { ZMBuf::$context[$this->cid]["cache"][$key] = $value; }
public function setCache($key, $value) { self::$context[$this->cid]["cache"][$key] = $value; }
public function getCQResponse() { return ZMBuf::$context[$this->cid]["cq_response"] ?? null; }
public function getCQResponse() { return self::$context[$this->cid]["cq_response"] ?? null; }
/**
* only can used by cq->message event function
@@ -109,13 +110,21 @@ class Context implements ContextInterface
case "private":
case "discuss":
$this->setCache("has_reply", true);
return CQAPI::quick_reply(ConnectionManager::get($this->getFrame()->fd), $this->getData(), $msg, $yield);
$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;
}
return false;
}
public function finalReply($msg, $yield = false) {
ZMBuf::$context[$this->cid]["cache"]["block_continue"] = true;
self::$context[$this->cid]["cache"]["block_continue"] = true;
if ($msg == "") return true;
return $this->reply($msg, $yield);
}
@@ -129,12 +138,24 @@ class Context implements ContextInterface
* @throws WaitTimeoutException
*/
public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") {
if ($prompt != "") $this->reply($prompt);
if (!isset($this->getData()["user_id"], $this->getData()["message"], $this->getData()["self_id"]))
throw new InvalidArgumentException("协程等待参数缺失");
Console::debug("==== 开始等待输入 ====");
if ($prompt != "") $this->reply($prompt);
try {
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]);
} catch (Exception $e) {
$r = false;
}
if ($r === false) {
throw new WaitTimeoutException($this, $timeout_prompt);
}
return $r["message"];
/*
$cid = Co::getuid();
$api_id = ZMBuf::$atomics["wait_msg_id"]->get();
ZMBuf::$atomics["wait_msg_id"]->add(1);
$api_id = ZMAtomic::get("wait_msg_id")->add(1);
$hang = [
"coroutine" => $cid,
"user_id" => $this->getData()["user_id"],
@@ -146,49 +167,57 @@ class Context implements ContextInterface
if ($hang["message_type"] == "group" || $hang["message_type"] == "discuss") {
$hang[$hang["message_type"] . "_id"] = $this->getData()[$this->getData()["message_type"] . "_id"];
}
ZMBuf::appendKey("wait_api", $api_id, $hang);
SpinLock::lock("wait_api");
$hw = LightCacheInside::get("wait_api", "wait_api") ?? [];
$hw[$api_id] = $hang;
LightCacheInside::set("wait_api", "wait_api", $hw);
SpinLock::unlock("wait_api");
$id = swoole_timer_after($timeout * 1000, function () use ($api_id, $timeout_prompt) {
$r = ZMBuf::get("wait_api")[$api_id] ?? null;
if ($r !== null) {
$r = LightCacheInside::get("wait_api", "wait_api")[$api_id] ?? null;
if (is_array($r)) {
Co::resume($r["coroutine"]);
}
});
Co::suspend();
$sess = ZMBuf::get("wait_api")[$api_id];
ZMBuf::unsetByValue("wait_api", $api_id);
SpinLock::lock("wait_api");
$hw = LightCacheInside::get("wait_api", "wait_api") ?? [];
$sess = $hw[$api_id];
unset($hw[$api_id]);
LightCacheInside::set("wait_api", "wait_api", $hw);
$result = $sess["result"];
if (isset($id)) swoole_timer_clear($id);
if ($result === null) throw new WaitTimeoutException($this, $timeout_prompt);
return $result;
return $result;*/
}
/**
* @param $arg
* @param $mode
* @param $prompt_msg
* @return mixed|string
* @throws InvalidArgumentException
* @throws WaitTimeoutException
*/
public function getArgs(&$arg, $mode, $prompt_msg) {
public function getArgs($mode, $prompt_msg) {
$arg = ctx()->getCache("match");
switch ($mode) {
case ZM_MATCH_ALL:
$p = $arg;
array_shift($p);
return trim(implode(" ", $p)) == "" ? $this->waitMessage($prompt_msg) : trim(implode(" ", $p));
case ZM_MATCH_NUMBER:
foreach ($arg as $k => $v) {
if (is_numeric($v)) {
array_splice($arg, $k, 1);
ctx()->setCache("match", $arg);
return $v;
}
}
return $this->waitMessage($prompt_msg);
case ZM_MATCH_FIRST:
if (isset($arg[1])) {
$a = $arg[1];
array_splice($arg, 1, 1);
if (isset($arg[0])) {
$a = $arg[0];
array_splice($arg, 0, 1);
ctx()->setCache("match", $arg);
return $a;
} else {
return $this->waitMessage($prompt_msg);
@@ -197,10 +226,28 @@ class Context implements ContextInterface
throw new InvalidArgumentException();
}
/**
* @param string $prompt_msg
* @return int|mixed|string
* @throws InvalidArgumentException
* @throws WaitTimeoutException
*/
public function getNextArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_FIRST, $prompt_msg); }
/**
* @param string $prompt_msg
* @return int|mixed|string
* @throws InvalidArgumentException
* @throws WaitTimeoutException
*/
public function getFullArg($prompt_msg = "") { return $this->getArgs(ZM_MATCH_ALL, $prompt_msg); }
public function cloneFromParent() {
set_coroutine_params(ZMBuf::$context[Co::getPcid()] ?? ZMBuf::$context[$this->cid]);
set_coroutine_params(self::$context[Co::getPcid()] ?? self::$context[$this->cid]);
return context();
}
public function copy() { return ZMBuf::$context[$this->cid]; }
public function copy() { return self::$context[$this->cid]; }
public function getOption() { return self::getCache("match"); }
}

View File

@@ -7,9 +7,9 @@ namespace ZM\Context;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use ZM\Connection\WSConnection;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Http\Response;
use ZM\Utils\ZMRobot;
use ZM\API\ZMRobot;
interface ContextInterface
{
@@ -26,7 +26,7 @@ interface ContextInterface
public function setData($data);
/** @return WSConnection */
/** @return ConnectionObject */
public function getConnection();
/** @return int|null */
@@ -97,12 +97,15 @@ interface ContextInterface
public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "");
/**
* @param $arg
* @param $mode
* @param $prompt_msg
* @return mixed
*/
public function getArgs(&$arg, $mode, $prompt_msg);
public function getArgs($mode, $prompt_msg);
public function getNextArg($prompt_msg = "");
public function getFullArg($prompt_msg = "");
public function setCache($key, $value);
@@ -115,4 +118,6 @@ interface ContextInterface
public function cloneFromParent();
public function copy();
public function getOption();
}

View File

@@ -1,15 +1,15 @@
<?php
<?php /** @noinspection PhpComposerExtensionStubsInspection */
namespace ZM\DB;
use Exception;
use framework\Console;
use framework\ZMBuf;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\MySQL\SqlPoolStorage;
use PDOException;
use PDOStatement;
use Swoole\Coroutine;
use Swoole\Database\PDOStatementProxy;
use ZM\Exception\DbException;
@@ -23,7 +23,7 @@ class DB
*/
public static function initTableList() {
if (!extension_loaded("mysqlnd")) throw new Exception("Can not find mysqlnd PHP extension.");
$result = self::rawQuery("select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='" . ZMBuf::globals("sql_config")["sql_database"] . "';", []);
$result = self::rawQuery("select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='" . ZMConfig::get("global", "sql_config")["sql_database"] . "';", []);
foreach ($result as $v) {
self::$table_list[] = $v['TABLE_NAME'];
}
@@ -35,11 +35,11 @@ class DB
* @return Table
* @throws DbException
*/
public static function table($table_name, $enable_cache = null) {
public static function table($table_name) {
if (Table::getTableInstance($table_name) === null) {
if (in_array($table_name, self::$table_list))
return new Table($table_name, $enable_cache ?? ZMBuf::globals("sql_config")["sql_enable_cache"]);
elseif(ZMBuf::$sql_pool !== null){
return new Table($table_name);
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");
@@ -62,26 +62,16 @@ class DB
* @throws DbException
*/
public static function unprepared($line) {
if (ZMBuf::get("sql_log") === true) {
$starttime = microtime(true);
}
try {
$conn = ZMBuf::$sql_pool->get();
$conn = SqlPoolStorage::$sql_pool->get();
if ($conn === false) {
ZMBuf::$sql_pool->put(null);
SqlPoolStorage::$sql_pool->put(null);
throw new DbException("无法连接SQL" . $line);
}
$result = $conn->query($line) === false ? false : true;
ZMBuf::$sql_pool->put($conn);
SqlPoolStorage::$sql_pool->put($conn);
return $result;
} catch (DBException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 5) .
"] " . $line . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
Console::warning($e->getMessage());
throw $e;
}
@@ -95,24 +85,21 @@ class DB
* @throws DbException
*/
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
if (ZMBuf::get("sql_log") === true) {
$starttime = microtime(true);
}
Console::debug("MySQL: ".$line);
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
try {
$conn = ZMBuf::$sql_pool->get();
$conn = SqlPoolStorage::$sql_pool->get();
if ($conn === false) {
ZMBuf::$sql_pool->put(null);
SqlPoolStorage::$sql_pool->put(null);
throw new DbException("无法连接SQL" . $line);
}
$ps = $conn->prepare($line);
if ($ps === false) {
ZMBuf::$sql_pool->put(null);
SqlPoolStorage::$sql_pool->put(null);
throw new DbException("SQL语句查询错误" . $line . ",错误信息:" . $conn->error);
} else {
if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) {
var_dump($ps);
ZMBuf::$sql_pool->put(null);
SqlPoolStorage::$sql_pool->put(null);
throw new DbException("语句查询错误!返回的不是 PDOStatement" . $line);
}
if ($params == []) $result = $ps->execute();
@@ -120,28 +107,14 @@ class DB
$result = $ps->execute([$params]);
} else $result = $ps->execute($params);
if ($result !== true) {
ZMBuf::$sql_pool->put(null);
SqlPoolStorage::$sql_pool->put(null);
throw new DBException("语句[$line]错误!" . $ps->errorInfo()[2]);
//echo json_encode(debug_backtrace(), 128 | 256);
}
ZMBuf::$sql_pool->put($conn);
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
SqlPoolStorage::$sql_pool->put($conn);
return $ps->fetchAll($fetch_mode);
}
} catch (DbException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
@@ -150,13 +123,6 @@ class DB
Console::warning($e->getMessage());
throw $e;
} catch (PDOException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");

View File

@@ -4,7 +4,6 @@
namespace ZM\DB;
use Framework\Console;
use ZM\Exception\DbException;
class SelectBody
@@ -46,17 +45,7 @@ class SelectBody
* @throws DbException
*/
public function fetchAll($fetch_mode = ZM_DEFAULT_FETCH_MODE) {
if ($this->table->isCacheEnabled()) {
$rr = md5(implode(",", $this->select_thing) . serialize($this->where_thing));
if (array_key_exists($rr, $this->table->cache)) {
Console::debug('SQL query cached: ' . $rr);
return $this->table->cache[$rr]->getResult();
}
}
$this->execute($fetch_mode);
if ($this->table->isCacheEnabled() && !in_array($rr, $this->table->cache)) {
$this->table->cache[$rr] = $this;
}
return $this->getResult();
}

View File

@@ -14,10 +14,7 @@ class Table
private static $table_instance = [];
private $enable_cache;
public function __construct($table_name, $enable_cache) {
$this->enable_cache = $enable_cache;
public function __construct($table_name) {
$this->table_name = $table_name;
self::$table_instance[$table_name] = $this;
}
@@ -75,5 +72,4 @@ class Table
*/
public function getTableName() { return $this->table_name; }
public function isCacheEnabled() { return $this->enable_cache; }
}
}

View File

@@ -1,201 +0,0 @@
<?php
namespace ZM\Event\CQ;
use Co;
use Doctrine\Common\Annotations\AnnotationException;
use Framework\Console;
use Framework\ZMBuf;
use ZM\Annotation\CQ\CQAfter;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\CQ\CQMessage;
use ZM\Connection\WSConnection;
use ZM\Event\EventHandler;
use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response;
use ZM\ModBase;
use ZM\ModHandleType;
class MessageEvent
{
private $function_call = false;
private $data;
private $circle;
/** @var WSConnection|Response */
private $connection;
public function __construct($data, $conn_or_response, $circle = 0) {
$this->data = $data;
$this->connection = $conn_or_response;
$this->circle = $circle;
}
/**
* @return bool
* @throws AnnotationException
*/
public function onBefore() {
$obj_list = ZMBuf::$events[CQBefore::class]["message"] ?? [];
foreach ($obj_list as $v) {
if ($v->level < 200) break;
EventHandler::callWithMiddleware(
$v->class,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if (!$r) context()->setCache("block_continue", true);
}
);
if (context()->getCache("block_continue") === true) return false;
}
foreach (ZMBuf::get("wait_api", []) as $k => $v) {
if (context()->getData()["user_id"] == $v["user_id"] &&
context()->getData()["self_id"] == $v["self_id"] &&
context()->getData()["message_type"] == $v["message_type"] &&
(context()->getData()[context()->getData()["message_type"] . "_id"] ?? context()->getData()["user_id"]) ==
($v[$v["message_type"] . "_id"] ?? $v["user_id"])) {
$v["result"] = context()->getData()["message"];
ZMBuf::appendKey("wait_api", $k, $v);
Co::resume($v["coroutine"]);
return false;
}
}
foreach (ZMBuf::$events[CQBefore::class]["message"] ?? [] as $v) {
if ($v->level >= 200) continue;
$c = $v->class;
if (ctx()->getCache("level") != 0) continue;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if (!$r) context()->setCache("block_continue", true);
}
);
if (context()->getCache("block_continue") === true) return false;
}
return true;
}
/**
* @throws AnnotationException
*/
public function onActivate() {
try {
$word = split_explode(" ", str_replace("\r", "", context()->getMessage()));
if (count(explode("\n", $word[0])) >= 2) {
$enter = explode("\n", context()->getMessage());
$first = split_explode(" ", array_shift($enter));
$word = array_merge($first, $enter);
foreach ($word as $k => $v) {
$word[$k] = trim($word[$k]);
}
}
/** @var ModBase[] $obj */
$obj = [];
foreach (ZMBuf::$events[CQCommand::class] ?? [] as $v) {
/** @var CQCommand $v */
if ($v->match == "" && $v->regexMatch == "" && $v->fullMatch == "") continue;
elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) &&
($v->discuss_id == 0 || ($v->discuss_id != 0 && $v->discuss_id == (context()->getData()["discuss_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"]))
) {
$c = $v->class;
$class_construct = [
"data" => context()->getData(),
"connection" => context()->getConnection()
];
if (!isset($obj[$c])) {
$obj[$c] = new $c($class_construct);
}
if ($word[0] != "" && $v->match == $word[0]) {
Console::debug("Calling $c -> {$v->method}");
$this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$word], function ($r) {
if (is_string($r)) context()->reply($r);
return true;
});
return;
} elseif (in_array($word[0], $v->alias)) {
Console::debug("Calling $c -> {$v->method}");
$this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$word], function ($r) {
if (is_string($r)) context()->reply($r);
return true;
});
return;
} elseif ($v->regexMatch != "" && ($args = matchArgs($v->regexMatch, context()->getMessage())) !== false) {
Console::debug("Calling $c -> {$v->method}");
$this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r) {
if (is_string($r)) context()->reply($r);
return true;
});
return;
} elseif ($v->fullMatch != "" && (preg_match("/".$v->fullMatch."/u", ctx()->getMessage(), $args)) != 0) {
Console::debug("Calling $c -> {$v->method}");
array_shift($args);
$this->function_call = EventHandler::callWithMiddleware($obj[$c], $v->method, $class_construct, [$args], function ($r) {
if (is_string($r)) context()->reply($r);
return true;
});
return;
}
}
}
foreach (ZMBuf::$events[CQMessage::class] ?? [] as $v) {
/** @var CQMessage $v */
if (
($v->message == '' || ($v->message != '' && $v->message == context()->getData()["message"])) &&
($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) &&
($v->discuss_id == 0 || ($v->discuss_id != 0 && $v->discuss_id == (context()->getData()["discuss_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) &&
($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"]))) {
$c = $v->class;
Console::debug("Calling CQMessage: $c -> {$v->method}");
if (!isset($obj[$c]))
$obj[$c] = new $c([
"data" => context()->getData(),
"connection" => $this->connection
], ModHandleType::CQ_MESSAGE);
EventHandler::callWithMiddleware($obj[$c], $v->method, [], [context()->getData()["message"]], function ($r) {
if (is_string($r)) context()->reply($r);
});
if (context()->getCache("block_continue") === true) return;
}
}
} catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
}
/**
* 在调用完事件后执行的
* @throws AnnotationException
*/
public function onAfter() {
context()->setCache("block_continue", null);
foreach (ZMBuf::$events[CQAfter::class]["message"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if (!$r) context()->setCache("block_continue", true);
}
);
if (context()->getCache("block_continue") === true) return false;
}
return true;
}
public function hasReply() {
return $this->function_call;
}
}

View File

@@ -1,79 +0,0 @@
<?php
namespace ZM\Event\CQ;
use Doctrine\Common\Annotations\AnnotationException;
use Framework\ZMBuf;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQMetaEvent;
use ZM\Connection\CQConnection;
use ZM\Event\EventHandler;
use ZM\Exception\WaitTimeoutException;
use ZM\ModBase;
use ZM\ModHandleType;
class MetaEvent
{
private $data;
/** @var CQConnection */
private $connection;
private $circle;
public function __construct($data, $connection, $circle = 0) {
$this->data = $data;
$this->connection = $connection;
$this->circle = $circle;
}
/**
* @return bool
* @throws AnnotationException
*/
public function onBefore() {
foreach (ZMBuf::$events[CQBefore::class]["meta_event"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if(!$r) context()->setCache("block_continue", true);
}
);
if(context()->getCache("block_continue") === true) return false;
}
return true;
}
/**
* @throws AnnotationException
*/
public function onActivate() {
try {
/** @var ModBase[] $obj */
$obj = [];
foreach (ZMBuf::$events[CQMetaEvent::class] ?? [] as $v) {
/** @var CQMetaEvent $v */
if (
($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == $this->data["meta_event_type"])) &&
($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"]))) {
$c = $v->class;
if (!isset($obj[$c]))
$obj[$c] = new $c([
"data" => $this->data,
"connection" => $this->connection
], ModHandleType::CQ_META_EVENT);
EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) {
if (is_string($r)) context()->reply($r);
});
if (context()->getCache("block_continue") === true) return;
}
}
} catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
}
}

View File

@@ -1,103 +0,0 @@
<?php
namespace ZM\Event\CQ;
use Doctrine\Common\Annotations\AnnotationException;
use Framework\ZMBuf;
use ZM\Annotation\CQ\CQAfter;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQNotice;
use ZM\Connection\CQConnection;
use ZM\Event\EventHandler;
use ZM\Exception\WaitTimeoutException;
use ZM\ModBase;
use ZM\ModHandleType;
class NoticeEvent
{
private $data;
/** @var CQConnection */
private $connection;
private $circle;
public function __construct($data, $connection, $circle = 0) {
$this->data = $data;
$this->connection = $connection;
$this->circle = $circle;
}
/**
* @return bool
* @throws AnnotationException
*/
public function onBefore() {
foreach (ZMBuf::$events[CQBefore::class]["notice"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if(!$r) context()->setCache("block_continue", true);
}
);
if(context()->getCache("block_continue") === true) return false;
}
return true;
}
/**
* @throws AnnotationException
*/
public function onActivate() {
try {
/** @var ModBase[] $obj */
$obj = [];
foreach (ZMBuf::$events[CQNotice::class] ?? [] as $v) {
/** @var CQNotice $v */
if (
($v->notice_type == '' || ($v->notice_type != '' && $v->notice_type == $this->data["notice_type"])) &&
($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($this->data["group_id"] ?? 0))) &&
($v->operator_id == 0 || ($v->operator_id != 0 && $v->operator_id == ($this->data["operator_id"] ?? 0)))) {
$c = $v->class;
if (!isset($obj[$c]))
$obj[$c] = new $c([
"data" => $this->data,
"connection" => $this->connection
], ModHandleType::CQ_NOTICE);
EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) {
if (is_string($r)) context()->reply($r);
});
if (context()->getCache("block_continue") === true) return;
}
}
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
}
/**
* @return bool
* @throws AnnotationException
*/
public function onAfter() {
foreach (ZMBuf::$events[CQAfter::class]["notice"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if(!$r) context()->setCache("block_continue", true);
}
);
if(context()->getCache("block_continue") === true) return false;
}
return true;
}
}

View File

@@ -1,103 +0,0 @@
<?php
namespace ZM\Event\CQ;
use Doctrine\Common\Annotations\AnnotationException;
use Framework\ZMBuf;
use ZM\Annotation\CQ\CQAfter;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQRequest;
use ZM\Connection\CQConnection;
use ZM\Event\EventHandler;
use ZM\Exception\WaitTimeoutException;
use ZM\ModBase;
use ZM\ModHandleType;
class RequestEvent
{
private $data;
/** @var CQConnection */
private $connection;
private $circle;
public function __construct($data, $connection, $circle = 0) {
$this->data = $data;
$this->connection = $connection;
$this->circle = $circle;
}
/**
* @return bool
* @throws AnnotationException
*/
public function onBefore() {
foreach (ZMBuf::$events[CQBefore::class]["request"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if(!$r) context()->setCache("block_continue", true);
}
);
if(context()->getCache("block_continue") === true) return false;
}
return true;
}
/**
* @throws AnnotationException
*/
public function onActivate() {
try {
/** @var ModBase[] $obj */
$obj = [];
foreach (ZMBuf::$events[CQRequest::class] ?? [] as $v) {
/** @var CQRequest $v */
if (
($v->request_type == '' || ($v->request_type != '' && $v->request_type == $this->data["request_type"])) &&
($v->sub_type == 0 || ($v->sub_type != 0 && $v->sub_type == $this->data["sub_type"])) &&
($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ($this->data["user_id"] ?? 0))) &&
($v->comment == 0 || ($v->comment != 0 && $v->comment == ($this->data["comment"] ?? 0)))) {
$c = $v->class;
if (!isset($obj[$c]))
$obj[$c] = new $c([
"data" => $this->data,
"connection" => $this->connection
], ModHandleType::CQ_REQUEST);
EventHandler::callWithMiddleware($obj[$c],$v->method, [], [], function($r) {
if (is_string($r)) context()->reply($r);
});
if (context()->getCache("block_continue") === true) return;
}
}
} catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
}
/**
* @return bool
* @throws AnnotationException
*/
public function onAfter() {
foreach (ZMBuf::$events[CQAfter::class]["request"] ?? [] as $v) {
$c = $v->class;
EventHandler::callWithMiddleware(
$c,
$v->method,
["data" => context()->getData(), "connection" => $this->connection],
[],
function ($r) {
if(!$r) context()->setCache("block_continue", true);
}
);
if(context()->getCache("block_continue") === true) return false;
}
return true;
}
}

View File

@@ -1,11 +0,0 @@
<?php
namespace ZM\Event;
interface Event
{
const SWOOLE = 1;
const CQ = 2;
}

View File

@@ -0,0 +1,178 @@
<?php
namespace ZM\Event;
use Doctrine\Common\Annotations\AnnotationException;
use Exception;
use ZM\Annotation\AnnotationBase;
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;
use ZM\Utils\ZMUtil;
class EventDispatcher
{
/** @var string */
private $class;
/** @var null|callable */
private $rule = null;
/** @var null|callable */
private $return_func = null;
/** @var bool */
private $log = false;
/** @var int */
private $eid = 0;
/**
* @param null $return_var
* @throws InterruptException
*/
public static function interrupt($return_var = null) {
throw new InterruptException($return_var);
}
public static function enableEventTrace($event_class) {
SpinLock::lock("_event_trace");
$list = LightCacheInside::get("wait_api", "event_trace");
$list[$event_class] = true;
LightCacheInside::set("wait_api", "event_trace", $list);
SpinLock::unlock("_event_trace");
}
public static function disableEventTrace($event_class) {
SpinLock::lock("_event_trace");
$list = LightCacheInside::get("wait_api", "event_trace");
unset($list[$event_class]);
LightCacheInside::set("wait_api", "event_trace", $list);
SpinLock::unlock("_event_trace");
}
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 = [];
}
if (isset($list[$class])) $this->log = true;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 开始分发事件: " . $class);
}
public function setRuleFunction(callable $rule = null) {
$this->rule = $rule;
return $this;
}
public function setReturnFunction(callable $return_func) {
$this->return_func = $return_func;
return $this;
}
public function dispatchEvents(...$params) {
try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
$result = $this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
if ($result !== false && is_callable($this->return_func)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
($this->return_func)($result);
}
}
return true;
} catch (InterruptException $e) {
return $e->return_var;
} catch (AnnotationException $e) {
return false;
}
}
/**
* @param AnnotationBase|null $v
* @param null $rule_func
* @param mixed ...$params
* @throws AnnotationException
* @throws InterruptException
* @return mixed
*/
public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) {
$q_c = $v->class;
$q_f = $v->method;
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在判断 " . $q_c . "::" . $q_f . " 方法下的 rule ...");
if ($rule_func !== null && !$rule_func($v)) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 判断为 false, 拒绝执行此方法。");
return false;
}
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 rule 为真,继续执行方法本身 ...");
if (isset(EventManager::$middleware_map[$q_c][$q_f])) {
$middlewares = EventManager::$middleware_map[$q_c][$q_f];
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法还绑定了 Middleware" . implode(", ", $middlewares));
$before_result = true;
$r = [];
foreach ($middlewares as $k => $middleware) {
if (!isset(EventManager::$middlewares[$middleware])) throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware}\"!");
$middleware_obj = EventManager::$middlewares[$middleware];
$before = $middleware_obj["class"];
//var_dump($middleware_obj);
$r[$k] = new $before();
$r[$k]->class = $q_c;
$r[$k]->method = $q_f;
if (isset($middleware_obj["before"])) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在前置事件,执行中 ...");
$rs = $middleware_obj["before"];
$before_result = $r[$k]->$rs(...$params);
if ($before_result === false) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 前置事件为 false停止执行原事件开始执行下一事件。");
break;
} else {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 前置事件为 true继续执行原事件。");
}
}
}
if ($before_result) {
try {
$q_o = ZMUtil::getModInstance($q_c);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
$result = $q_o->$q_f(...$params);
} catch (Exception $e) {
if ($e instanceof InterruptException) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 检测到事件阻断调用,正在跳出事件分发器 ...");
throw $e;
}
if ($this->log) Console::verbose("[事件分发{$this->eid}] 方法 " . $q_c . "::" . $q_f . " 执行过程中抛出了异常,正在倒序查找 Middleware 中的捕获方法 ...");
for ($i = count($middlewares) - 1; $i >= 0; --$i) {
$middleware_obj = EventManager::$middlewares[$middlewares[$i]];
if (!isset($middleware_obj["exceptions"])) continue;
foreach ($middleware_obj["exceptions"] as $name => $method) {
if ($e instanceof $name) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 方法 " . $q_c . "::" . $q_f . " 的异常 " . get_class($e) . " 被 Middleware:" . $middlewares[$i] . " 下的 " . get_class($r[$i]) . "::" . $method . " 捕获。");
$r[$i]->$method($e);
self::interrupt();
}
}
}
throw $e;
}
for ($i = count($middlewares) - 1; $i >= 0; --$i) {
$middleware_obj = EventManager::$middlewares[$middlewares[$i]];
if (isset($middleware_obj["after"], $r[$i])) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 存在后置事件,执行中 ...");
$r[$i]->{$middleware_obj["after"]}(...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] Middleware 后置事件执行完毕!");
}
}
return $result;
}
return false;
} else {
$q_o = ZMUtil::getModInstance($q_c);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
return $q_o->$q_f(...$params);
}
}
}

View File

@@ -1,320 +0,0 @@
<?php
namespace ZM\Event;
use Co;
use Doctrine\Common\Annotations\AnnotationException;
use Error;
use Exception;
use Framework\Console;
use Framework\ZMBuf;
use ZM\Event\Swoole\{MessageEvent, RequestEvent, WorkerStartEvent, WSCloseEvent, WSOpenEvent};
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\WebSocket\Frame;
use ZM\Annotation\CQ\CQAPIResponse;
use ZM\Annotation\CQ\CQAPISend;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Connection\ConnectionManager;
use ZM\Connection\CQConnection;
use ZM\Http\MiddlewareInterface;
use ZM\Http\Response;
use Framework\DataProvider;
use ZM\Utils\ZMUtil;
class EventHandler
{
/**
* @param $event_name
* @param $param0
* @param null $param1
* @throws AnnotationException
*/
public static function callSwooleEvent($event_name, $param0, $param1 = null) {
//$starttime = microtime(true);
unset(ZMBuf::$context[Co::getCid()]);
$event_name = strtolower($event_name);
switch ($event_name) {
case "workerstart":
try {
register_shutdown_function(function () use ($param0) {
$error = error_get_last();
if ($error["type"] != 0) {
Console::error("Internal fatal error: " . $error["message"] . " at " . $error["file"] . "({$error["line"]})");
}
DataProvider::saveBuffer();
/** @var Server $param0 */
if (ZMBuf::$server === null) $param0->shutdown();
else ZMBuf::$server->shutdown();
});
ZMBuf::$server = $param0;
$r = (new WorkerStartEvent($param0, $param1))->onActivate();
Console::log("\n=== Worker #" . $param0->worker_id . " 已启动 ===\n", "gold");
$r->onAfter();
self::startTick();
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
ZMUtil::stop();
return;
} catch (Error $e) {
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();
}
break;
case "message":
/** @var Frame $param1 */
/** @var Server $param0 */
$conn = ConnectionManager::get($param1->fd);
set_coroutine_params(["server" => $param0, "frame" => $param1, "connection" => $conn]);
try {
(new MessageEvent($param0, $param1))->onActivate()->onAfter();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Fatal error when calling $event_name: " . $error_msg);
Console::stackTrace();
}
break;
case "request":
try {
set_coroutine_params(["request" => $param0, "response" => $param1]);
(new RequestEvent($param0, $param1))->onActivate()->onAfter();
} catch (Exception $e) {
/** @var Response $param1 */
$param1->status(500);
Console::info($param0->server["remote_addr"] . ":" . $param0->server["remote_port"] .
" [" . $param1->getStatusCode() . "] " . $param0->server["request_uri"]
);
if (!$param1->isEnd()) $param1->end("Internal server error: " . $e->getMessage());
Console::error("Internal server exception (500), caused by " . get_class($e));
Console::log($e->getTraceAsString(), "gray");
} catch (Error $e) {
/** @var Response $param1 */
$param1->status(500);
Console::info($param0->server["remote_addr"] . ":" . $param0->server["remote_port"] .
" [" . $param1->getStatusCode() . "] " . $param0->server["request_uri"]
);
$doc = "Internal server error<br>";
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
if (ZMBuf::$atomics["info_level"]->get() >= 4) $doc .= $error_msg;
if (!$param1->isEnd()) $param1->end($doc);
Console::error("Internal server error (500): " . $error_msg);
Console::log($e->getTraceAsString(), "gray");
}
break;
case "open":
/** @var Request $param1 */
set_coroutine_params(["server" => $param0, "request" => $param1, "fd" => $param1->fd]);
try {
(new WSOpenEvent($param0, $param1))->onActivate()->onAfter();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Fatal error when calling $event_name: " . $error_msg);
Console::stackTrace();
}
break;
case "close":
set_coroutine_params(["server" => $param0, "fd" => $param1]);
try {
(new WSCloseEvent($param0, $param1))->onActivate()->onAfter();
} catch (Error $e) {
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
Console::error("Fatal error when calling $event_name: " . $error_msg);
Console::stackTrace();
}
break;
}
//Console::info(Console::setColor("Event: " . $event_name . " 运行了 " . round(microtime(true) - $starttime, 5) . " 秒", "gold"));
}
/**
* @param $event_data
* @param $conn_or_response
* @param int $level
* @return bool
* @throws AnnotationException
*/
public static function callCQEvent($event_data, $conn_or_response, int $level = 0) {
ctx()->setCache("level", $level);
if ($level >= 5) {
Console::warning("Recursive call reached " . $level . " times");
Console::stackTrace();
return false;
}
$starttime = microtime(true);
switch ($event_data["post_type"]) {
case "message":
$event = new CQ\MessageEvent($event_data, $conn_or_response, $level);
if ($event->onBefore()) $event->onActivate();
$event->onAfter();
return $event->hasReply();
break;
case "notice":
$event = new CQ\NoticeEvent($event_data, $conn_or_response, $level);
if ($event->onBefore()) $event->onActivate();
$event->onAfter();
return true;
case "request":
$event = new CQ\RequestEvent($event_data, $conn_or_response, $level);
if ($event->onBefore()) $event->onActivate();
$event->onAfter();
return true;
case "meta_event":
$event = new CQ\MetaEvent($event_data, $conn_or_response, $level);
if ($event->onBefore()) $event->onActivate();
return true;
}
unset($starttime);
return false;
}
/**
* @param $req
* @throws AnnotationException
*/
public static function callCQResponse($req) {
Console::debug("收到来自API连接的回复" . json_encode($req, 128 | 256));
$status = $req["status"];
$retcode = $req["retcode"];
$data = $req["data"];
if (isset($req["echo"]) && ZMBuf::array_key_exists("sent_api", $req["echo"])) {
$origin = ZMBuf::get("sent_api")[$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]);
if (isset(ZMBuf::$events[CQAPIResponse::class][$req["retcode"]])) {
list($c, $method) = ZMBuf::$events[CQAPIResponse::class][$req["retcode"]];
$class = new $c(["data" => $origin["data"]]);
call_user_func_array([$class, $method], [$origin["data"], $req]);
}
$origin_ctx = ctx()->copy();
ctx()->setCache("action", $origin["data"]["action"] ?? "unknown");
ctx()->setData($origin["data"]);
foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) {
if (($v->action == "" || $v->action == ctx()->getCache("action")) && $v->with_result) {
$c = $v->class;
self::callWithMiddleware($c, $v->method, context()->copy(), [ctx()->getCache("action"), $origin["data"]["params"] ?? [], ctx()->getRobotId()]);
if (context()->getCache("block_continue") === true) break;
}
}
set_coroutine_params($origin_ctx);
if (($origin["func"] ?? null) !== null) {
call_user_func($origin["func"], $response, $origin["data"]);
} elseif (($origin["coroutine"] ?? false) !== false) {
$p = ZMBuf::get("sent_api");
$p[$req["echo"]]["result"] = $response;
ZMBuf::set("sent_api", $p);
Co::resume($origin['coroutine']);
}
ZMBuf::unsetByValue("sent_api", $req["echo"]);
}
}
public static function callCQAPISend($reply, ?CQConnection $connection) {
$action = $reply["action"] ?? null;
if ($action === null) {
Console::warning("API 激活事件异常!");
return;
}
if (ctx() === null) $content = [];
else $content = ctx()->copy();
go(function () use ($action, $reply, $connection, $content) {
set_coroutine_params($content);
context()->setCache("action", $action);
context()->setCache("reply", $reply);
foreach (ZMBuf::$events[CQAPISend::class] ?? [] as $k => $v) {
if (($v->action == "" || $v->action == $action) && !$v->with_result) {
$c = $v->class;
self::callWithMiddleware($c, $v->method, context()->copy(), [$reply["action"], $reply["params"] ?? [], $connection->getQQ()]);
if (context()->getCache("block_continue") === true) break;
}
}
});
}
/**
* @param $c
* @param $method
* @param array $class_construct
* @param array $func_args
* @param null $after_call
* @return mixed|null
* @throws AnnotationException
* @throws Exception
*/
public static function callWithMiddleware($c, $method, array $class_construct, array $func_args, $after_call = null) {
$return_value = null;
$plain_class = is_object($c) ? get_class($c) : $c;
if (isset(ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method])) {
$middlewares = ZMBuf::$events[MiddlewareInterface::class][$plain_class][$method];
$before_result = true;
$r = [];
foreach ($middlewares as $k => $middleware) {
if (!isset(ZMBuf::$events[MiddlewareClass::class][$middleware])) throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$middleware}\"!");
$middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middleware];
$before = $middleware_obj["class"];
$r[$k] = new $before();
$r[$k]->class = is_object($c) ? get_class($c) : $c;
$r[$k]->method = $method;
if (isset($middleware_obj["before"])) {
$before_result = call_user_func_array([$r[$k], $middleware_obj["before"]], $func_args);
if ($before_result === false) break;
}
}
if ($before_result) {
try {
if (is_object($c)) $class = $c;
elseif ($class_construct == []) $class = ZMUtil::getModInstance($c);
else $class = new $c($class_construct);
$result = call_user_func_array([$class, $method], $func_args);
if (is_callable($after_call))
$return_value = call_user_func_array($after_call, [$result]);
} catch (Exception $e) {
for ($i = count($middlewares) - 1; $i >= 0; --$i) {
$middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middlewares[$i]];
if (!isset($middleware_obj["exceptions"])) continue;
foreach ($middleware_obj["exceptions"] as $name => $method) {
if ($e instanceof $name) {
$r[$i]->$method($e);
context()->setCache("block_continue", true);
}
}
if (context()->getCache("block_continue") === true) return $return_value;
}
throw $e;
}
}
for ($i = count($middlewares) - 1; $i >= 0; --$i) {
$middleware_obj = ZMBuf::$events[MiddlewareClass::class][$middlewares[$i]];
if (isset($middleware_obj["after"], $r[$i]))
call_user_func_array([$r[$i], $middleware_obj["after"]], $func_args);
}
} else {
if (is_object($c)) $class = $c;
elseif ($class_construct == []) $class = ZMUtil::getModInstance($c);
else $class = new $c($class_construct);
$result = call_user_func_array([$class, $method], $func_args);
if (is_callable($after_call))
$return_value = call_user_func_array($after_call, [$result]);
}
return $return_value;
}
private static function startTick() {
Console::debug("Starting " . count(ZMBuf::get("paused_tick", [])) . " custom tick function");
foreach (ZMBuf::get("paused_tick", []) as $cid) {
Co::resume($cid);
}
}
}

Some files were not shown because too many files have changed in this diff Show More