mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-04 23:35:35 +08:00
Compare commits
44 Commits
2.0.0-beta
...
2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80de2a552 | ||
|
|
5b85ec15e9 | ||
|
|
420f326f11 | ||
|
|
f361a675af | ||
|
|
18c09beacb | ||
|
|
c15d320ef6 | ||
|
|
58da612121 | ||
|
|
64ec34d54d | ||
|
|
eeb952035a | ||
|
|
e78485273d | ||
|
|
9bd07e5a66 | ||
|
|
4deb814ff2 | ||
|
|
619baf1691 | ||
|
|
bc0bb9b6b0 | ||
|
|
42d9c97711 | ||
|
|
81365173d2 | ||
|
|
ba5b793db7 | ||
|
|
1ffb30a471 | ||
|
|
cc31a1654d | ||
|
|
dfca486b64 | ||
|
|
754c2846fe | ||
|
|
eed670cb50 | ||
|
|
9e824d960f | ||
|
|
992b6020a5 | ||
|
|
d51dbef437 | ||
|
|
944a9e849b | ||
|
|
09a11821b2 | ||
|
|
dbfe2c9c17 | ||
|
|
7f058638bd | ||
|
|
63e0594199 | ||
|
|
4a4bc697d6 | ||
|
|
8edc3f337b | ||
|
|
c04130fed1 | ||
|
|
7fefcb850a | ||
|
|
1fe54d4b94 | ||
|
|
c460b37d14 | ||
|
|
13a32bec79 | ||
|
|
0f5786c8c4 | ||
|
|
4ed046769f | ||
|
|
690980f72d | ||
|
|
f025eeb34a | ||
|
|
d642f50ef1 | ||
|
|
2900754307 | ||
|
|
dffeac668d |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,3 +6,7 @@ zm.json
|
||||
/zm_data/
|
||||
composer.lock
|
||||
/resources/server.phar
|
||||
/distribute/
|
||||
/bin/.phpunit.result.cache
|
||||
/resources/zhamao.service
|
||||
.phpunit.result.cache
|
||||
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,28 +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
|
||||
|
||||
83
README.md
83
README.md
@@ -7,76 +7,65 @@
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
|
||||
[](https://packagist.org/packages/zhamao/framework)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## 开发者注意
|
||||
**此版本目前无法正常运行,还在持续开发中,请勿使用!!!**
|
||||
|
||||
**2.0 版本开发完毕后,会发布测试版和并入 master 分支,敬请关注。**
|
||||
|
||||
[]()
|
||||
**开发者 QQ 群:670821194**
|
||||
|
||||
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
|
||||
|
||||
**2.0 版本如果有问题请第一时间加群反馈!**
|
||||
|
||||
## 简介
|
||||
zhamao-framework 是一个 PHP Swoole 的聊天机器人框架,兼容 OneBot 标准,它会对微信公众号等终端收到的消息进行解析处理,并以模块化的形式进行开发,来完成机器人的自然语言对话等功能。
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
|
||||
除了起到解析消息的作用,炸毛框架 还提供了完整的 WebSocket + HTTP 服务器,你还能用此框架构建出高性能的 API 接口服务器。
|
||||
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
|
||||
```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-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。目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
|
||||
@@ -88,6 +77,8 @@ Pages托管:[https://framework.zhamao.xin/](https://framework.zhamao.xin/)
|
||||
### 支付宝
|
||||

|
||||
|
||||
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ,我们会作为您捐助了本项目!
|
||||
|
||||
## 关于
|
||||
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
|
||||
|
||||
@@ -96,3 +87,7 @@ Pages托管:[https://framework.zhamao.xin/](https://framework.zhamao.xin/)
|
||||
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
|
||||
|
||||
项目框架采用 Apache-2.0 协议开源,在分发或重写修改等操作时需遵守协议。项目模块部分(`Module` 文件夹) 在非借鉴框架内代码时可不遵守 Apache-2.0 协议进行分发和修改(声明版权)。
|
||||
|
||||
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
|
||||
|
||||

|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"description": "High performance QQ robot and web server development framework",
|
||||
"minimum-stability": "stable",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.0.0-b1",
|
||||
"version": "2.0.0",
|
||||
"extra": {},
|
||||
"authors": [
|
||||
{
|
||||
"name": "whale",
|
||||
@@ -21,30 +22,26 @@
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"ext-mbstring": "*",
|
||||
"doctrine/annotations": "~1.10",
|
||||
"ext-json": "*",
|
||||
"ext-posix": "*",
|
||||
"ext-ctype": "*",
|
||||
"psy/psysh": "@stable",
|
||||
"symfony/polyfill-ctype": "^1.20",
|
||||
"symfony/polyfill-mbstring": "^1.20",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/polyfill-ctype": "^1.18",
|
||||
"zhamao/connection-manager": "*@dev",
|
||||
"zhamao/console": "*@dev",
|
||||
"zhamao/config": "*@dev",
|
||||
"zhamao/console": "^1.0",
|
||||
"zhamao/config": "^1.0",
|
||||
"zhamao/request": "*@dev",
|
||||
"symfony/routing": "^5.1"
|
||||
"symfony/routing": "^5.1",
|
||||
"symfony/polyfill-php80": "^1.20"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-pdo": "Allows framework connecting with mysql server",
|
||||
"ext-redis": "Allows framework connecting with redis server",
|
||||
"ext-inotify": "Enable file watcher feature in framework"
|
||||
"ext-ctype": "*",
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Custom\\": "src/Custom",
|
||||
"ZM\\": "src/ZM",
|
||||
"Module\\": "src/Module"
|
||||
"ZM\\": "src/ZM"
|
||||
},
|
||||
"files": [
|
||||
"src/ZM/global_functions.php"
|
||||
@@ -61,15 +58,5 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.3",
|
||||
"swoole/ide-helper": "@dev"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "path",
|
||||
"url": "/Users/jerry/project/git-project/zhamao-console"
|
||||
},
|
||||
{
|
||||
"type": "path",
|
||||
"url": "/Users/jerry/project/git-project/zhamao-lock"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ $config['swoole'] = [
|
||||
|
||||
/** 轻量字符串缓存,默认开启 */
|
||||
$config['light_cache'] = [
|
||||
"size" => 1024, //最多允许储存的条数(需要2的倍数)
|
||||
"max_strlen" => 16384, //单行字符串最大长度(需要2的倍数)
|
||||
"hash_conflict_proportion" => 0.6, //Hash冲突率(越大越好,但是需要的内存更多)
|
||||
"persistence_path" => $config['zm_data']."_cache.json",
|
||||
'size' => 1024, //最多允许储存的条数(需要2的倍数)
|
||||
'max_strlen' => 16384, //单行字符串最大长度(需要2的倍数)
|
||||
'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多)
|
||||
'persistence_path' => $config['zm_data'].'_cache.json',
|
||||
'auto_save_interval' => 900
|
||||
];
|
||||
|
||||
@@ -50,8 +50,6 @@ $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
|
||||
@@ -108,14 +106,12 @@ $config['server_event_handler_class'] = [
|
||||
\ZM\Event\ServerEventHandler::class,
|
||||
];
|
||||
|
||||
/** 注册自定义指令的类 */
|
||||
$config['command_register_class'] = [
|
||||
//\Custom\Command\CustomCommand::class
|
||||
];
|
||||
|
||||
/** 服务器启用的外部第三方和内部插件 */
|
||||
$config['modules'] = [
|
||||
'onebot' => true, // QQ机器人事件解析器,如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭
|
||||
'onebot' => [
|
||||
'status' => true,
|
||||
'single_bot_mode' => false
|
||||
], // QQ机器人事件解析器,如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭
|
||||
];
|
||||
|
||||
return $config;
|
||||
|
||||
@@ -1,17 +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"]
|
||||
@@ -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
1
docs/FAQ.md
Normal file
@@ -0,0 +1 @@
|
||||
# FAQ
|
||||
3
docs/advanced/index.md
Normal file
3
docs/advanced/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 进阶开发
|
||||
## 深入
|
||||
还没填坑,敬请期待!
|
||||
39
docs/advanced/to-v2.md
Normal file
39
docs/advanced/to-v2.md
Normal 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
88
docs/assets/css/extra.css
Normal 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
BIN
docs/assets/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/assets/logos.png
Normal file
BIN
docs/assets/logos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
3
docs/component/index.md
Normal file
3
docs/component/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 框架组件
|
||||
|
||||
还没写到这里,不着急
|
||||
56
docs/event/index.md
Normal file
56
docs/event/index.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 事件和注解
|
||||
|
||||
## 注解事件概念
|
||||
|
||||
我们知道事件,是一个底层的 event loop 收到消息后调用对应的各类方法的一个模型,比如给机器人发送消息后框架要做的就是指定到一个你定义的函数上,处理你的业务逻辑代码。比如在默认模块中,提供了 **你好** 的回复:**你好啊,我是由炸毛框架构建的机器人!**。这项简单回复的任务就是一个事件的触发到响应的全过程。
|
||||
|
||||
**注解**(Annotation)又称标注,Java 最早在 2004 年的 JDK 5 中引入的一种注释机制。目前 PHP 官方版本并未提供内置元注解和注解概念,但我们通过 `ReflectionClass` 反射类解析 PHP 代码注释从而实现了自己的一套注解机制。如果你没有写过 Java,并且不了解注解是什么,你可以理解为对 function 或 class 的一个修饰,因为传统的 PHP 代码逻辑我们都知道,不能简单给原先存在的函数贴标签,就比如,你不能在原本的 PHP 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。
|
||||
|
||||
在常见框架如 Spring,Swoft 等代码结构里面,注解更是其核心的存在。
|
||||
|
||||
在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 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` 等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
|
||||
|
||||
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
|
||||
23
docs/guide/OneBot实例.md
Normal file
23
docs/guide/OneBot实例.md
Normal 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
142
docs/guide/基本配置.md
Normal 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
134
docs/guide/安装.md
Normal 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 发行版,如 CentOS,Fedora,Arch 等暂时还没有经过严格的测试需要哪些依赖,大体和 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/) 部分!
|
||||
4
docs/guide/快速上手-http.md
Normal file
4
docs/guide/快速上手-http.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# 快速上手 - HTTP 服务器篇
|
||||
|
||||
HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~
|
||||
|
||||
228
docs/guide/快速上手-机器人.md
Normal file
228
docs/guide/快速上手-机器人.md
Normal 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 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。
|
||||
|
||||
66
docs/guide/注册事件响应.md
Normal file
66
docs/guide/注册事件响应.md
Normal 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()` 是炸毛框架内的上下文获取方式,每条用户聊天信息发过来,被炸毛框架收到,都会创建一次上下文,同时这次聊天的全部信息,比如用户的 ID(QQ 号码),发消息的时间,如果是群消息的话所在的群号等等,都被存到了上下文中。`ctx()` 获取的是一个上下文对象,内部有许多可操作上下文的方法,其中代码的 `getNextArg()`,作用是根据空格分隔获取命令中的下一个参数。
|
||||
|
||||
在之前我们知道:`天气 北京` 是我们发送的消息,我们要获取到用户发送的参数 `北京`。`getNextArg()` 是框架封装好的一个快速获取下一个参数的方法,我们这里直接使用它来获取。
|
||||
|
||||
对于 `getNextArg()` 中的文本,可为空,不为空的时候如果用户只发送天气两个字,机器人还是会响应,但是它会询问你这句话,然后你回复机器人“北京”,这里 `$city` 变量就接受到并赋值为“北京”了,代码会继续执行,和直接一次性发送机器人“天气 北京”是一个效果,此为框架封装的消息会话机制,以贴近自然会话的方式来编写代码逻辑。
|
||||
|
||||
最后,函数直接返回了一个字符串,作为事件的响应,炸毛框架会自动处理并调用机器人客户端的接口,最后返回给用户消息。这里也可以使用上下文的 `ctx()->reply("xxx")` 方法替代,不返回字符串。注意两者只能选择一种方式,取决于开发者的开发习惯。
|
||||
|
||||
<chat-box>
|
||||
) 天气 北京
|
||||
( 北京 天气情况:2020年12月22日,晴,-2~9℃ blablabla
|
||||
) 天气
|
||||
( 请告诉我你要查询的城市
|
||||
) 北京
|
||||
( 北京 天气情况:2020年12月22日,晴,-2~9℃ blablabla
|
||||
</chat-box>
|
||||
68
docs/guide/编写模块.md
Normal file
68
docs/guide/编写模块.md
Normal 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
130
docs/index.md
Normal 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>
|
||||
85
docs/javascripts/config.js
Normal file
85
docs/javascripts/config.js
Normal 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
220
docs/update/v1.md
Normal 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
3
docs/update/v2.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 更新日志(v2 版本)
|
||||
|
||||
> 暂未发布正式版。
|
||||
74
mkdocs.yml
Normal file
74
mkdocs.yml
Normal 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 © 2019 - 2020 CrazyBot Team <span class="tx-switch">
|
||||
<button data-md-color-scheme="default"><code>默认模式</code></button>
|
||||
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
|
||||
</span>
|
||||
|
||||
<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/
|
||||
10
resources/html/subdir/index.html
Normal file
10
resources/html/subdir/index.html
Normal 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>
|
||||
@@ -8,7 +8,9 @@ use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Store\Redis\ZMRedis;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
/**
|
||||
* Class Hello
|
||||
@@ -40,11 +42,20 @@ class Hello
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
|
||||
* @CQCommand(".reload",user_id=627577391)
|
||||
*/
|
||||
public function reload() {
|
||||
ctx()->reply("重启中...");
|
||||
ZMUtil::reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand("我是谁")
|
||||
*/
|
||||
public function whoami() {
|
||||
$user = ctx()->getRobot()->setCallback(true)->getLoginInfo();
|
||||
$user = ctx()->getRobot()->getLoginInfo();
|
||||
return "你是" . $user["data"]["nickname"] . ",QQ号是" . $user["data"]["user_id"];
|
||||
}
|
||||
|
||||
@@ -121,6 +132,14 @@ class Hello
|
||||
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链接,因为这个绑定的事件,你可以根据你自己的需求进行修改
|
||||
* @OnSwooleEvent(type="open",rule="connectIsDefault()")
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
namespace Module\Middleware;
|
||||
|
||||
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;
|
||||
@@ -19,7 +20,7 @@ class TimerMiddleware implements MiddlewareInterface
|
||||
private $starttime;
|
||||
|
||||
/**
|
||||
* @Before()
|
||||
* @HandleBefore()
|
||||
* @return bool
|
||||
*/
|
||||
public function onBefore() {
|
||||
@@ -28,9 +29,16 @@ class TimerMiddleware implements MiddlewareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @After()
|
||||
* @HandleAfter()
|
||||
*/
|
||||
public function onAfter() {
|
||||
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleException(\Exception::class)
|
||||
*/
|
||||
public function onException() {
|
||||
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ trait CQAPI
|
||||
public function processWebsocketAPI($connection, $reply, $function = false) {
|
||||
$api_id = ZMAtomic::get("wait_msg_id")->add(1);
|
||||
$reply["echo"] = $api_id;
|
||||
//EventHandler::callCQAPISend($reply, $connection);
|
||||
SpinLock::lock("wait_api");
|
||||
$r = LightCacheInside::get("wait_api", "wait_api");
|
||||
$r[$api_id] = [
|
||||
|
||||
@@ -24,7 +24,7 @@ class ZMRobot
|
||||
/** @var ConnectionObject|null */
|
||||
private $connection;
|
||||
|
||||
private $callback = null;
|
||||
private $callback = true;
|
||||
private $prefix = 0;
|
||||
|
||||
/**
|
||||
@@ -50,6 +50,9 @@ class ZMRobot
|
||||
return new ZMRobot($r[array_rand($r)]);
|
||||
}
|
||||
|
||||
public static function getFirst() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZMRobot[]
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ use ZM\Console\Console;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use ZM\Annotation\Http\{After, Before, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
|
||||
use ZM\Annotation\Interfaces\Level;
|
||||
use ZM\Annotation\Module\Closed;
|
||||
use ZM\Utils\DataProvider;
|
||||
@@ -287,8 +287,8 @@ class AnnotationParser
|
||||
foreach ($reflection_class->getMethods() as $vss) {
|
||||
$method_annotations = $this->reader->getMethodAnnotations($vss);
|
||||
foreach ($method_annotations as $vsss) {
|
||||
if ($vsss instanceof Before) $result["before"] = $vss->getName();
|
||||
if ($vsss instanceof After) $result["after"] = $vss->getName();
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,12 @@ class CQCommand extends AnnotationBase implements Level
|
||||
public $pattern = "";
|
||||
/** @var string */
|
||||
public $regex = "";
|
||||
/** @var string */
|
||||
public $start_with = "";
|
||||
/** @var string */
|
||||
public $end_with = "";
|
||||
/** @var string */
|
||||
public $keyword = "";
|
||||
/** @var string[] */
|
||||
public $alias = [];
|
||||
/** @var string */
|
||||
|
||||
@@ -9,11 +9,11 @@ use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* Class Before
|
||||
* Class HandleAfter
|
||||
* @package ZM\Annotation\Http
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class Before extends AnnotationBase
|
||||
class HandleAfter extends AnnotationBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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 After
|
||||
* Class HandleBefore
|
||||
* @package ZM\Annotation\Http
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class After extends AnnotationBase
|
||||
class HandleBefore extends AnnotationBase
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,11 @@ use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* Class SwooleSetup
|
||||
* Class ZMSetup
|
||||
* @package ZM\Annotation\Swoole
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class SwooleSetup extends AnnotationBase
|
||||
class OnSetup extends AnnotationBase
|
||||
{
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use ZM\Annotation\AnnotationBase;
|
||||
* @Annotation
|
||||
* @Target("ALL")
|
||||
*/
|
||||
class OnWorkerStart extends AnnotationBase
|
||||
class OnStart extends AnnotationBase
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -9,12 +9,12 @@ use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* Class HandleEvent
|
||||
* Class SwooleHandler
|
||||
* @package ZM\Annotation\Swoole
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class HandleEvent extends AnnotationBase
|
||||
class SwooleHandler extends AnnotationBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
@@ -37,15 +37,15 @@ class RunServerCommand extends Command
|
||||
|
||||
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 and staging ! </error>");
|
||||
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());
|
||||
(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;
|
||||
|
||||
@@ -5,15 +5,12 @@ namespace ZM;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use TypeError;
|
||||
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\Config\ZMConfig;
|
||||
use ZM\Utils\DataProvider;
|
||||
|
||||
class ConsoleApplication extends Application
|
||||
@@ -25,11 +22,7 @@ class ConsoleApplication extends Application
|
||||
|
||||
public function initEnv() {
|
||||
$this->selfCheck();
|
||||
$this->addCommands([
|
||||
new RunServerCommand(), //运行主服务的指令控制器
|
||||
new InitCommand(), //初始化用的,用于项目初始化和phar初始化
|
||||
new PureHttpCommand()
|
||||
]);
|
||||
|
||||
//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__ . "/../../"));
|
||||
@@ -47,21 +40,41 @@ class ConsoleApplication extends Application
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_dir(DataProvider::getWorkingDir() . '/src/')) {
|
||||
die("Unable to find source directory.\nMaybe you need to run \"init\"?");
|
||||
}
|
||||
ZMConfig::setDirectory(DataProvider::getWorkingDir().'/config');
|
||||
ZMConfig::env($args["env"] ?? "");
|
||||
if(ZMConfig::get("global") === false) die("Global config load failed: ".ZMConfig::$last_error);
|
||||
|
||||
$this->addCommands([
|
||||
new RunServerCommand(), //运行主服务的指令控制器
|
||||
new InitCommand(), //初始化用的,用于项目初始化和phar初始化
|
||||
new PureHttpCommand() //纯HTTP服务器指令
|
||||
]);
|
||||
/*
|
||||
$command_register = ZMConfig::get("global", "command_register_class") ?? [];
|
||||
foreach($command_register as $v) {
|
||||
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");
|
||||
if (!($obj instanceof Command)) throw new TypeError("Command register class must be extended by Symfony\\Component\\Console\\Command\\Command");
|
||||
$this->add($obj);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,11 +91,11 @@ class ConsoleApplication extends Application
|
||||
}
|
||||
|
||||
private function selfCheck() {
|
||||
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
|
||||
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 (!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");
|
||||
|
||||
@@ -28,7 +28,7 @@ class Context implements ContextInterface
|
||||
/**
|
||||
* @return swoole_server|null
|
||||
*/
|
||||
public function getServer() { return self::$context[$this->cid]["server"] ?? null; }
|
||||
public function getServer() { return self::$context[$this->cid]["server"] ?? server(); }
|
||||
|
||||
/**
|
||||
* @return Frame|null
|
||||
@@ -149,7 +149,7 @@ class Context implements ContextInterface
|
||||
} catch (Exception $e) {
|
||||
$r = false;
|
||||
}
|
||||
if($r === false) {
|
||||
if ($r === false) {
|
||||
throw new WaitTimeoutException($this, $timeout_prompt);
|
||||
}
|
||||
return $r["message"];
|
||||
@@ -203,7 +203,6 @@ class Context implements ContextInterface
|
||||
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) {
|
||||
@@ -215,9 +214,9 @@ class Context implements ContextInterface
|
||||
}
|
||||
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 {
|
||||
@@ -227,6 +226,22 @@ 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(self::$context[Co::getPcid()] ?? self::$context[$this->cid]);
|
||||
return context();
|
||||
|
||||
@@ -103,6 +103,10 @@ interface ContextInterface
|
||||
*/
|
||||
public function getArgs($mode, $prompt_msg);
|
||||
|
||||
public function getNextArg($prompt_msg = "");
|
||||
|
||||
public function getFullArg($prompt_msg = "");
|
||||
|
||||
public function setCache($key, $value);
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,8 +7,12 @@ namespace ZM\Event;
|
||||
use Doctrine\Common\Annotations\AnnotationException;
|
||||
use Exception;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\CQ\CQMetaEvent;
|
||||
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
|
||||
@@ -19,16 +23,45 @@ class EventDispatcher
|
||||
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() {
|
||||
throw new InterruptException('interrupt');
|
||||
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) {
|
||||
@@ -43,14 +76,17 @@ class EventDispatcher
|
||||
|
||||
public function dispatchEvents(...$params) {
|
||||
try {
|
||||
|
||||
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
|
||||
$result = $this->dispatchEvent($v, $this->rule, ...$params);
|
||||
if ($result !== false && is_callable($this->return_func)) ($this->return_func)($result);
|
||||
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 null;
|
||||
return $e->return_var;
|
||||
} catch (AnnotationException $e) {
|
||||
return false;
|
||||
}
|
||||
@@ -60,16 +96,22 @@ class EventDispatcher
|
||||
* @param AnnotationBase|null $v
|
||||
* @param null $rule_func
|
||||
* @param mixed ...$params
|
||||
* @return bool
|
||||
* @throws AnnotationException
|
||||
* @throws InterruptException
|
||||
* @return mixed
|
||||
*/
|
||||
public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) {
|
||||
$q_c = $v->class;
|
||||
$q_f = $v->method;
|
||||
if ($rule_func !== null && !$rule_func($v)) return false;
|
||||
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) {
|
||||
@@ -81,22 +123,34 @@ class EventDispatcher
|
||||
$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) break;
|
||||
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) throw $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();
|
||||
}
|
||||
@@ -107,7 +161,9 @@ class EventDispatcher
|
||||
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;
|
||||
@@ -115,6 +171,7 @@ class EventDispatcher
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use Swoole\Process;
|
||||
use Swoole\Timer;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Annotation\Swoole\OnWorkerStart;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
@@ -24,7 +24,7 @@ use ZM\Console\Console;
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\Server;
|
||||
use Swoole\WebSocket\Frame;
|
||||
use ZM\Annotation\Swoole\HandleEvent;
|
||||
use ZM\Annotation\Swoole\SwooleHandler;
|
||||
use ZM\Console\TermColor;
|
||||
use ZM\Context\Context;
|
||||
use ZM\Context\ContextInterface;
|
||||
@@ -33,6 +33,7 @@ use ZM\Exception\DbException;
|
||||
use ZM\Framework;
|
||||
use ZM\Http\Response;
|
||||
use ZM\Module\QQBot;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\MySQL\SqlPoolStorage;
|
||||
use ZM\Store\Redis\ZMRedisPool;
|
||||
use ZM\Store\ZMBuf;
|
||||
@@ -44,7 +45,7 @@ use ZM\Utils\ZMUtil;
|
||||
class ServerEventHandler
|
||||
{
|
||||
/**
|
||||
* @HandleEvent("start")
|
||||
* @SwooleHandler("start")
|
||||
*/
|
||||
public function onStart() {
|
||||
global $terminal_id;
|
||||
@@ -87,14 +88,14 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("shutdown")
|
||||
* @SwooleHandler("shutdown")
|
||||
*/
|
||||
public function onShutdown() {
|
||||
Console::debug("正在关闭 Master 进程,pid=" . posix_getpid());
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("WorkerStop")
|
||||
* @SwooleHandler("WorkerStop")
|
||||
* @param $server
|
||||
* @param $worker_id
|
||||
*/
|
||||
@@ -103,7 +104,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("WorkerStart")
|
||||
* @SwooleHandler("WorkerStart")
|
||||
* @param Server $server
|
||||
* @param $worker_id
|
||||
*/
|
||||
@@ -205,12 +206,12 @@ class ServerEventHandler
|
||||
EventManager::registerTimerTick(); //启动计时器
|
||||
//ZMBuf::unsetCache("wait_start");
|
||||
set_coroutine_params(["server" => $server, "worker_id" => $worker_id]);
|
||||
$dispatcher = new EventDispatcher(OnWorkerStart::class);
|
||||
$dispatcher = new EventDispatcher(OnStart::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
return server()->worker_id === $v->worker_id || $v->worker_id === -1;
|
||||
});
|
||||
$dispatcher->dispatchEvents($server, $worker_id);
|
||||
Console::debug("@OnWorkerStart 执行完毕");
|
||||
Console::debug("@OnStart 执行完毕");
|
||||
} catch (Exception $e) {
|
||||
Console::error("Worker加载出错!停止服务!");
|
||||
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
@@ -243,7 +244,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("message")
|
||||
* @SwooleHandler("message")
|
||||
* @param $server
|
||||
* @param Frame $frame
|
||||
*/
|
||||
@@ -266,9 +267,9 @@ class ServerEventHandler
|
||||
}
|
||||
});
|
||||
try {
|
||||
$starttime = microtime(true);
|
||||
//$starttime = microtime(true);
|
||||
$dispatcher->dispatchEvents($conn);
|
||||
Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!");
|
||||
//Console::success("Used ".round((microtime(true) - $starttime) * 1000, 3)." ms!");
|
||||
} catch (Exception $e) {
|
||||
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
|
||||
Console::error("Uncaught exception " . get_class($e) . " when calling \"message\": " . $error_msg);
|
||||
@@ -282,7 +283,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("request")
|
||||
* @SwooleHandler("request")
|
||||
* @param $request
|
||||
* @param $response
|
||||
*/
|
||||
@@ -350,13 +351,13 @@ class ServerEventHandler
|
||||
else
|
||||
$response->end("Internal server error.");
|
||||
}
|
||||
Console::error("Internal server error (500), caused by " . get_class($e));
|
||||
Console::error("Internal server error (500), caused by " . get_class($e).": ".$e->getMessage());
|
||||
Console::log($e->getTraceAsString(), "gray");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("open")
|
||||
* @SwooleHandler("open")
|
||||
* @param $server
|
||||
* @param Request $request
|
||||
*/
|
||||
@@ -369,6 +370,7 @@ class ServerEventHandler
|
||||
$conn = ManagerGM::get($request->fd);
|
||||
set_coroutine_params(["server" => $server, "request" => $request, "connection" => $conn, "fd" => $request->fd]);
|
||||
$conn->setOption("connect_id", strval($request->header["x-self-id"]) ?? "");
|
||||
|
||||
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -380,6 +382,11 @@ class ServerEventHandler
|
||||
}
|
||||
});
|
||||
try {
|
||||
if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) {
|
||||
if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) {
|
||||
LightCacheInside::set("connect", "conn_fd", $request->fd);
|
||||
}
|
||||
}
|
||||
$dispatcher->dispatchEvents($conn);
|
||||
} catch (Exception $e) {
|
||||
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
|
||||
@@ -394,7 +401,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("close")
|
||||
* @SwooleHandler("close")
|
||||
* @param $server
|
||||
* @param $fd
|
||||
*/
|
||||
@@ -404,6 +411,8 @@ class ServerEventHandler
|
||||
if ($conn === null) return;
|
||||
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
|
||||
set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]);
|
||||
|
||||
|
||||
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -415,6 +424,11 @@ class ServerEventHandler
|
||||
}
|
||||
});
|
||||
try {
|
||||
if ($conn->getName() === 'qq' && ZMConfig::get("global", "modules")["onebot"]["status"] === true) {
|
||||
if (ZMConfig::get("global", "modules")["onebot"]["single_bot_mode"]) {
|
||||
LightCacheInside::set("connect", "conn_fd", -1);
|
||||
}
|
||||
}
|
||||
$dispatcher->dispatchEvents($conn);
|
||||
} catch (Exception $e) {
|
||||
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
|
||||
@@ -429,7 +443,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("pipeMessage")
|
||||
* @SwooleHandler("pipeMessage")
|
||||
* @param $server
|
||||
* @param $src_worker_id
|
||||
* @param $data
|
||||
@@ -462,7 +476,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("task")
|
||||
* @SwooleHandler("task")
|
||||
*/
|
||||
public function onTask() {
|
||||
}
|
||||
@@ -504,7 +518,7 @@ class ServerEventHandler
|
||||
|
||||
//加载插件
|
||||
$plugins = ZMConfig::get("global", "modules") ?? [];
|
||||
if (!isset($plugins["onebot"])) $plugins["onebot"] = true;
|
||||
if (!isset($plugins["onebot"])) $plugins["onebot"] = ["status" => true, "single_bot_mode" => false];
|
||||
|
||||
if ($plugins["onebot"]) {
|
||||
$obj = new OnSwooleEvent();
|
||||
@@ -514,6 +528,11 @@ class ServerEventHandler
|
||||
$obj->level = 99999;
|
||||
$obj->rule = 'connectIsQQ()';
|
||||
EventManager::addEvent(OnSwooleEvent::class, $obj);
|
||||
if ($plugins["onebot"]["single_bot_mode"]) {
|
||||
LightCacheInside::set("connect", "conn_fd", -1);
|
||||
} else {
|
||||
LightCacheInside::set("connect", "conn_fd", -2);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: 编写加载外部插件的方式
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class DbException extends Exception
|
||||
class DbException extends ZMException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class InterruptException extends Exception
|
||||
class InterruptException extends ZMException
|
||||
{
|
||||
public $return_var = null;
|
||||
|
||||
public function __construct($return_var = null, $message = "", $code = 0, Throwable $previous = null) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->return_var = $return_var;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidArgumentException extends Exception
|
||||
class InvalidArgumentException extends ZMException
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotInitializedException extends Exception
|
||||
class NotInitializedException extends ZMException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -12,7 +11,7 @@ use Throwable;
|
||||
* @package ZM\Exception
|
||||
* @since 1.2
|
||||
*/
|
||||
class RobotNotFoundException extends Exception
|
||||
class RobotNotFoundException extends ZMException
|
||||
{
|
||||
public function __construct($message = "", $code = 0, Throwable $previous = null) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
class WaitTimeoutException extends Exception
|
||||
class WaitTimeoutException extends ZMException
|
||||
{
|
||||
public $module;
|
||||
|
||||
|
||||
12
src/ZM/Exception/ZMException.php
Normal file
12
src/ZM/Exception/ZMException.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Exception;
|
||||
|
||||
|
||||
use Exception;
|
||||
|
||||
class ZMException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace ZM;
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationReader;
|
||||
use Exception;
|
||||
use ZM\Annotation\Swoole\SwooleSetup;
|
||||
use ZM\Annotation\Swoole\OnSetup;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Event\ServerEventHandler;
|
||||
@@ -15,13 +15,12 @@ use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
use ZM\Utils\DataProvider;
|
||||
use Framework\RemoteShell;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionMethod;
|
||||
use Swoole\Runtime;
|
||||
use Swoole\WebSocket\Server;
|
||||
use ZM\Annotation\Swoole\HandleEvent;
|
||||
use ZM\Annotation\Swoole\SwooleHandler;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
@@ -48,6 +47,11 @@ class Framework
|
||||
//定义常量
|
||||
include_once "global_defines.php";
|
||||
|
||||
ZMConfig::setDirectory(DataProvider::getWorkingDir() . '/config');
|
||||
ZMConfig::setEnv($args["env"] ?? "");
|
||||
if (ZMConfig::get("global") === false) {
|
||||
die ("Global config load failed: " . ZMConfig::$last_error . "\nPlease init first!\n");
|
||||
}
|
||||
ZMAtomic::init();
|
||||
try {
|
||||
ManagerGM::init(ZMConfig::get("global", "swoole")["max_connection"] ?? 2048, 0.5, [
|
||||
@@ -68,7 +72,7 @@ class Framework
|
||||
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
|
||||
$this->server_set = ZMConfig::get("global", "swoole");
|
||||
Console::init(
|
||||
ZMConfig::get("global", "info_level"),
|
||||
ZMConfig::get("global", "info_level") ?? 2,
|
||||
self::$server,
|
||||
$args["log-theme"] ?? "default",
|
||||
($o = ZMConfig::get("console_color")) === false ? [] : $o
|
||||
@@ -112,16 +116,15 @@ class Framework
|
||||
// 注册 Swoole Server 的事件
|
||||
$this->registerServerEvents();
|
||||
$r = ZMConfig::get("global", "light_cache") ?? [
|
||||
"size" => 1024,
|
||||
"max_strlen" => 8192,
|
||||
"hash_conflict_proportion" => 0.6,
|
||||
"persistence_path" => realpath(DataProvider::getDataFolder() . "_cache.json"),
|
||||
"auto_save_interval" => 900
|
||||
];
|
||||
"size" => 1024,
|
||||
"max_strlen" => 8192,
|
||||
"hash_conflict_proportion" => 0.6,
|
||||
"persistence_path" => realpath(DataProvider::getDataFolder() . "_cache.json"),
|
||||
"auto_save_interval" => 900
|
||||
];
|
||||
LightCache::init($r);
|
||||
LightCacheInside::init();
|
||||
SpinLock::init($r["size"]);
|
||||
self::$server->start();
|
||||
} catch (Exception $e) {
|
||||
Console::error("Framework初始化出现错误,请检查!");
|
||||
Console::error($e->getMessage());
|
||||
@@ -129,6 +132,10 @@ class Framework
|
||||
}
|
||||
}
|
||||
|
||||
public function start() {
|
||||
self::$server->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全局配置文件里读取注入系统事件的类
|
||||
* @throws ReflectionException
|
||||
@@ -148,11 +155,11 @@ class Framework
|
||||
$method_annotations = $reader->getMethodAnnotations($vs);
|
||||
if ($method_annotations != []) {
|
||||
$annotation = $method_annotations[0];
|
||||
if ($annotation instanceof HandleEvent) {
|
||||
if ($annotation instanceof SwooleHandler) {
|
||||
$annotation->class = $v;
|
||||
$annotation->method = $vs->getName();
|
||||
$event_list[strtolower($annotation->event)] = $annotation;
|
||||
} elseif ($annotation instanceof SwooleSetup) {
|
||||
} elseif ($annotation instanceof OnSetup) {
|
||||
$annotation->class = $v;
|
||||
$annotation->method = $vs->getName();
|
||||
$c = new $v();
|
||||
@@ -213,14 +220,6 @@ class Framework
|
||||
case 'disable-console-input':
|
||||
if ($y) $terminal_id = null;
|
||||
break;
|
||||
case 'remote-shell':
|
||||
if ($y) {
|
||||
$host = "127.0.0.1";
|
||||
$port = 9599;
|
||||
RemoteShell::listen(self::$server, $host, $port);
|
||||
Console::log(Console::setColor("正在监听" . $host . ":" . strval($port) . "的调试接口,请注意安全", "yellow"));
|
||||
}
|
||||
break;
|
||||
case 'log-error':
|
||||
if ($y) Console::setLevel(0);
|
||||
break;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
namespace ZM\Http;
|
||||
|
||||
|
||||
use ZM\Console\Console;
|
||||
|
||||
class Response
|
||||
{
|
||||
|
||||
@@ -184,7 +186,8 @@ class Response
|
||||
* @return mixed
|
||||
*/
|
||||
public function redirect($location, $http_code = null) {
|
||||
return $this->redirect($location, $http_code);
|
||||
$this->is_end = true;
|
||||
return $this->response->redirect($location, $http_code);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,6 @@ use ZM\Utils\CoMessage;
|
||||
/**
|
||||
* Class QQBot
|
||||
* @package ZM\Module
|
||||
* @ExternalModule("onebot")
|
||||
*/
|
||||
class QQBot
|
||||
{
|
||||
@@ -80,15 +79,25 @@ class QQBot
|
||||
//分发CQCommand事件
|
||||
$dispatcher = new EventDispatcher(CQCommand::class);
|
||||
$dispatcher->setRuleFunction(function (CQCommand $v) use ($word) {
|
||||
if ($v->match == "" && $v->pattern == "" && $v->regex == "") return false;
|
||||
if(array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) return false;
|
||||
elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == ctx()->getUserId())) &&
|
||||
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (ctx()->getGroupId() ?? 0))) &&
|
||||
($v->message_type == '' || ($v->message_type != '' && $v->message_type == ctx()->getMessageType()))
|
||||
) {
|
||||
if(($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
|
||||
array_shift($word);
|
||||
ctx()->setCache("match", $word);
|
||||
return true;
|
||||
} elseif ($v->pattern != "") {
|
||||
} elseif ($v->start_with != "" && mb_strpos(ctx()->getMessage(), $v->start_with) === 0) {
|
||||
ctx()->setCache("match", [mb_substr(ctx()->getMessage(), mb_strlen($v->start_with))]);
|
||||
return true;
|
||||
} elseif ($v->end_with != "" && strlen(ctx()->getMessage()) == (strripos(ctx()->getMessage(), $v->end_with) + strlen($v->end_with))) {
|
||||
ctx()->setCache("match", [substr(ctx()->getMessage(), 0, strripos(ctx()->getMessage(), $v->end_with))]);
|
||||
return true;
|
||||
} elseif ($v->keyword != "" && mb_strpos(ctx()->getMessage(), $v->keyword) !== false) {
|
||||
ctx()->setCache("match", explode($v->keyword, ctx()->getMessage()));
|
||||
return true;
|
||||
}elseif ($v->pattern != "") {
|
||||
$match = matchArgs($v->pattern, ctx()->getMessage());
|
||||
if($match !== false) {
|
||||
ctx()->setCache("match", $match);
|
||||
|
||||
@@ -92,7 +92,7 @@ class LightCache
|
||||
} elseif (is_bool($value)) {
|
||||
$data_type = "bool";
|
||||
$value = json_encode($value);
|
||||
}else {
|
||||
} else {
|
||||
throw new Exception("Only can set string, array and int");
|
||||
}
|
||||
try {
|
||||
@@ -126,7 +126,7 @@ class LightCache
|
||||
throw new Exception("Only can set string, array and int");
|
||||
}
|
||||
try {
|
||||
if(self::$kv_table->get($key) === false) return false;
|
||||
if (self::$kv_table->get($key) === false) return false;
|
||||
return self::$kv_table->set($key, [
|
||||
"value" => $value,
|
||||
"data_type" => $data_type
|
||||
@@ -170,7 +170,7 @@ class LightCache
|
||||
}
|
||||
|
||||
public static function savePersistence() {
|
||||
if(self::$kv_table === null) return;
|
||||
if (self::$kv_table === null) return;
|
||||
$r = [];
|
||||
foreach (self::$kv_table as $k => $v) {
|
||||
if ($v["expire"] === -2) {
|
||||
@@ -178,8 +178,11 @@ class LightCache
|
||||
$r[$k] = self::parseGet($v);
|
||||
}
|
||||
}
|
||||
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
|
||||
if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
|
||||
if(self::$config["persistence_path"] == "") return;
|
||||
if (file_exists(self::$config["persistence_path"])) {
|
||||
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
|
||||
if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkExpire($key) {
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ZM\Store;
|
||||
|
||||
use Exception;
|
||||
use Swoole\Table;
|
||||
use ZM\Exception\ZMException;
|
||||
|
||||
class LightCacheInside
|
||||
{
|
||||
@@ -15,9 +16,11 @@ class LightCacheInside
|
||||
public static $last_error = '';
|
||||
|
||||
public static function init() {
|
||||
self::$kv_table["wait_api"] = new Table(2, 0);
|
||||
self::$kv_table["wait_api"] = new Table(3, 0);
|
||||
self::$kv_table["wait_api"]->column("value", Table::TYPE_STRING, 65536);
|
||||
$result = self::$kv_table["wait_api"]->create();
|
||||
self::$kv_table["connect"] = new Table(8, 0);
|
||||
self::$kv_table["connect"]->column("value", Table::TYPE_STRING, 256);
|
||||
$result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create();
|
||||
if ($result === false) {
|
||||
self::$last_error = '系统内存不足,申请失败';
|
||||
return $result;
|
||||
@@ -25,8 +28,14 @@ class LightCacheInside
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param string $key
|
||||
* @return mixed|null
|
||||
* @throws ZMException
|
||||
*/
|
||||
public static function get(string $table, string $key) {
|
||||
if (!isset(self::$kv_table[$table])) throw new Exception("not initialized LightCache");
|
||||
if (!isset(self::$kv_table[$table])) throw new ZMException("not initialized LightCache");
|
||||
$r = self::$kv_table[$table]->get($key);
|
||||
return $r === false ? null : json_decode($r["value"], true);
|
||||
}
|
||||
@@ -36,10 +45,10 @@ class LightCacheInside
|
||||
* @param string $key
|
||||
* @param string|array|int $value
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
* @throws ZMException
|
||||
*/
|
||||
public static function set(string $table, string $key, $value) {
|
||||
if (self::$kv_table === null) throw new Exception("not initialized LightCache");
|
||||
if (self::$kv_table === null) throw new ZMException("not initialized LightCache");
|
||||
try {
|
||||
return self::$kv_table[$table]->set($key, [
|
||||
"value" => json_encode($value, 256)
|
||||
|
||||
@@ -29,6 +29,7 @@ class ZMAtomic
|
||||
}
|
||||
self::$atomics["stop_signal"] = new Atomic(0);
|
||||
self::$atomics["wait_msg_id"] = new Atomic(0);
|
||||
self::$atomics["_event_id"] = new Atomic(0);
|
||||
for ($i = 0; $i < 10; ++$i) {
|
||||
self::$atomics["_tmp_" . $i] = new Atomic(0);
|
||||
}
|
||||
|
||||
@@ -1,263 +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) {
|
||||
$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) . ')';
|
||||
}
|
||||
}
|
||||
24
src/ZM/Utils/SingletonTrait.php
Normal file
24
src/ZM/Utils/SingletonTrait.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
|
||||
trait SingletonTrait
|
||||
{
|
||||
/**
|
||||
* @var self
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public static function getInstance() {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,7 @@ class Terminal
|
||||
case 'psysh':
|
||||
if (Framework::$argv["disable-coroutine"]) {
|
||||
(new Shell())->run();
|
||||
}
|
||||
else
|
||||
} else
|
||||
Console::error("Only \"--disable-coroutine\" mode can use psysh!!!");
|
||||
return true;
|
||||
case 'bc':
|
||||
|
||||
@@ -37,9 +37,7 @@ class ZMUtil
|
||||
if (($v["result"] ?? false) === null && isset($v["coroutine"])) Co::resume($v["coroutine"]);
|
||||
}
|
||||
LightCacheInside::unset("wait_api", "wait_api");
|
||||
foreach (server()->connections as $v) {
|
||||
server()->close($v);
|
||||
}
|
||||
LightCache::savePersistence();
|
||||
//DataProvider::saveBuffer();
|
||||
Timer::clearAll();
|
||||
server()->reload();
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Swoole\Coroutine;
|
||||
use ZM\API\ZMRobot;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Context\Context;
|
||||
use ZM\Event\EventManager;
|
||||
use ZM\Exception\RobotNotFoundException;
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Framework;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\ZMBuf;
|
||||
use ZM\Utils\DataProvider;
|
||||
use Swoole\Coroutine\System;
|
||||
use ZM\Context\ContextInterface;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
|
||||
function phar_classloader($p) {
|
||||
@@ -181,11 +185,11 @@ function getAnnotations() {
|
||||
$s = debug_backtrace()[1];
|
||||
//echo json_encode($s, 128|256);
|
||||
$list = [];
|
||||
foreach(EventManager::$events as $event => $v) {
|
||||
foreach($v as $ks => $vs) {
|
||||
foreach (EventManager::$events as $event => $v) {
|
||||
foreach ($v as $ks => $vs) {
|
||||
//echo get_class($vs).": ".$vs->class." => ".$vs->method.PHP_EOL;
|
||||
if($vs->class == $s["class"] && $vs->method == $s["function"]) {
|
||||
$list[get_class($vs)][]=$vs;
|
||||
if ($vs->class == $s["class"] && $vs->method == $s["function"]) {
|
||||
$list[get_class($vs)][] = $vs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +231,7 @@ function ctx() {
|
||||
}
|
||||
}
|
||||
|
||||
function debug($msg) { Console::debug($msg); }
|
||||
function zm_debug($msg) { Console::debug($msg); }
|
||||
|
||||
function onebot_target_id_name($message_type) {
|
||||
return ($message_type == "group" ? "group_id" : "user_id");
|
||||
@@ -268,6 +272,22 @@ function server() {
|
||||
return Framework::$server;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ZMRobot
|
||||
* @throws RobotNotFoundException
|
||||
* @throws ZMException
|
||||
*/
|
||||
function bot() {
|
||||
if (($conn = LightCacheInside::get("connect", "conn_fd")) == -2) {
|
||||
return ZMRobot::getRandom();
|
||||
} elseif ($conn != -1) {
|
||||
if (($obj = ManagerGM::get($conn)) !== null) return new ZMRobot($obj);
|
||||
else throw new RobotNotFoundException("单机器人连接模式可能连接了多个机器人!");
|
||||
} else {
|
||||
throw new RobotNotFoundException("没有任何机器人连接到框架!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Module\Example\Hello;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionException;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Swoole\OnWorkerStart;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Console\Console;
|
||||
|
||||
class AnnotationParserRegisterTest extends TestCase
|
||||
@@ -34,8 +34,8 @@ class AnnotationParserRegisterTest extends TestCase
|
||||
public function testAnnotation() {
|
||||
ob_start();
|
||||
$gen = $this->parser->generateAnnotationEvents();
|
||||
$m = $gen[OnWorkerStart::class][0]->method;
|
||||
$class = $gen[OnWorkerStart::class][0]->class;
|
||||
$m = $gen[OnStart::class][0]->method;
|
||||
$class = $gen[OnStart::class][0]->class;
|
||||
$c = new $class();
|
||||
try {
|
||||
$c->$m();
|
||||
|
||||
Reference in New Issue
Block a user