mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-04 15:25:38 +08:00
Compare commits
76 Commits
2.0.0-beta
...
2.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31876025e | ||
|
|
ae8b0acdaa | ||
|
|
20ca3e7416 | ||
|
|
a1bfc031b8 | ||
|
|
8a6f8f54a5 | ||
|
|
19f0bffcd8 | ||
|
|
6337b626d6 | ||
|
|
7434bac94e | ||
|
|
ac45ab0dec | ||
|
|
8d248f301e | ||
|
|
937d31ccd9 | ||
|
|
ef263a5d1e | ||
|
|
bf03dfda38 | ||
|
|
6ba209c4c7 | ||
|
|
e7d86537be | ||
|
|
44979c670f | ||
|
|
747ecf28ea | ||
|
|
f5d5929cb9 | ||
|
|
6e866001d6 | ||
|
|
775c275288 | ||
|
|
86a0e1a2ca | ||
|
|
aae79aacb5 | ||
|
|
991af3d728 | ||
|
|
25ae4e02c2 | ||
|
|
5a6642e217 | ||
|
|
d00f6ee3a2 | ||
|
|
1e6cc9cd84 | ||
|
|
b7b94db5ea | ||
|
|
b0054d7884 | ||
|
|
22942c33cf | ||
|
|
1f9c5eeeb4 | ||
|
|
44337d2ad4 | ||
|
|
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
|
||||
|
||||
85
README.md
85
README.md
@@ -1,82 +1,73 @@
|
||||
<div align="center">
|
||||
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
|
||||
<h2>炸毛框架</h2>
|
||||
炸毛框架 (zhamao-frameowork) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
|
||||
炸毛框架 (zhamao-framework) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
|
||||
|
||||
[]()
|
||||
[](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>
|
||||
|
||||
## 开发者注意
|
||||
**此版本目前无法正常运行,还在持续开发中,请勿使用!!!**
|
||||
**开发者 QQ 群:670821194**
|
||||
|
||||
**2.0 版本开发完毕后,会发布测试版和并入 master 分支,敬请关注。**
|
||||
|
||||
[]()
|
||||
**当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!**
|
||||
|
||||
**2.0 版本如果有问题请第一时间加群反馈!**
|
||||
|
||||
有关 3.0 版本的最新情况,请看这里:[Issue #22](https://github.com/zhamao-robot/zhamao-framework/issues/22)
|
||||
|
||||
## 简介
|
||||
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/)
|
||||
备用链接:[https://docs-v2.zhamao.me/](https://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 +79,8 @@ Pages托管:[https://framework.zhamao.xin/](https://framework.zhamao.xin/)
|
||||
### 支付宝
|
||||

|
||||
|
||||
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ,我们会作为您捐助了本项目!
|
||||
|
||||
## 关于
|
||||
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
|
||||
|
||||
@@ -96,3 +89,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,12 @@
|
||||
"description": "High performance QQ robot and web server development framework",
|
||||
"minimum-stability": "stable",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.0.0-b1",
|
||||
"version": "2.1.2",
|
||||
"extra": {
|
||||
"exclude_annotate": [
|
||||
"src/ZM"
|
||||
]
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "whale",
|
||||
@@ -21,55 +26,32 @@
|
||||
],
|
||||
"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"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"ZMTest\\": "test/ZMTest"
|
||||
},
|
||||
"files": [
|
||||
"test/ZMTest/Mock/mock.php"
|
||||
]
|
||||
},
|
||||
"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/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
docs-v2.zhamao.me
|
||||
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 @@
|
||||
# 进阶开发
|
||||
## 深入
|
||||
还没填坑,敬请期待!
|
||||
29
docs/advanced/inside-class.md
Normal file
29
docs/advanced/inside-class.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 内部类文件手册
|
||||
|
||||
这个章节写明了在框架使用过程中可能涉及到的框架内部或 Swoole、其他 composer 依赖组件的内部类,这里会根据类的命名空间一一说明。
|
||||
|
||||
## Swoole\Http\Request
|
||||
|
||||
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping` 或 `@OnRequestEvent()` 两个注解下可用,用作获取 GET、POST参数,上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
|
||||
|
||||
### 属性
|
||||
|
||||
- `$fd`:获取当前连接的文件描述符 ID。
|
||||
- `$header`:`HTTP` 请求的头部信息。类型为数组,所有 `key` 均为小写。
|
||||
- `$server`:`HTTP` 请求相关的服务器信息。
|
||||
- `$cookie`:获取 Cookies。
|
||||
- `$get`:获取 GET 参数。
|
||||
- `$post`:获取 POST 参数。
|
||||
- `$files`:获取上传的文件信息
|
||||
|
||||
### 方法
|
||||
|
||||
- `rawContent()`:获取 POST 包原始二进制内容,相当于原生 PHP 的 ` file_get_contents("php://input");` 。
|
||||
- `getData()`:获取完整的原始 `Http` 请求报文。包括 `Http Header` 和 `Http Body`
|
||||
|
||||
### 示例
|
||||
|
||||
```php
|
||||
TODO:先放一放。
|
||||
```
|
||||
|
||||
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")` 了。
|
||||
103
docs/assets/css/extra.css
Normal file
103
docs/assets/css/extra.css
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
.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-banner {
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.1);
|
||||
width: max-content;
|
||||
margin: 8px auto;
|
||||
padding: 4px 14px;
|
||||
border-radius: 8px;
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.md-typeset .admonition, .md-typeset details {
|
||||
font-size: .72rem;
|
||||
}
|
||||
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/img/Untitled Diagram.png
Normal file
BIN
docs/assets/img/Untitled Diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/assets/img/diagram3.dbb4e32e.png
Normal file
BIN
docs/assets/img/diagram3.dbb4e32e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/assets/img/diagram4.16ce39ca.png
Normal file
BIN
docs/assets/img/diagram4.16ce39ca.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 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 |
299
docs/component/context.md
Normal file
299
docs/component/context.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# 上下文
|
||||
|
||||
上下文作为整个框架中最重要的内容之一,请务必理解和完整地阅读此部分!
|
||||
|
||||
一个上下文描述了一个事件和所关联的对象的环境。例如:你在处理 HTTP 请求的 `@RequestMapping` 绑定的事件中,你需要获取请求的 HTTP 头和 Cookie,再比如你在处理 QQ 机器人发来的命令 `@CQCommand("随机数")` 的时候,在这个方法内,你需要获取发来的人的 QQ 号码。以上我们将处理以上运行环境的对象叫做上下文。
|
||||
|
||||
由于 Swoole 的协程加持,我们利用了协程 ID 绑定对象来进行构造上下文。
|
||||
|
||||
以默认的机器人收发消息为例,通过对默认模块的了解,我们可以知道,在绑定 `@CQCommand` 等类似事件后,你可以用上下文获取发来这条消息的人的 QQ 号码:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("你好")
|
||||
*/
|
||||
public function hello() {
|
||||
$user_id = ctx()->getUserId();
|
||||
ctx()->reply("你好啊,".$user_id.",很高兴认识你!");
|
||||
}
|
||||
```
|
||||
|
||||
`context()` 就是获取上下文对象的全局函数,它还有简写:`ctx()`。
|
||||
|
||||
当然,上下文中的方法不是每个都能在任何时候使用的。例如 `getUserId()` 你不能在 `@RequestMapping` 注解的函数中使用,因为它不是机器人消息的上下文。下面说明上下文对象的方法中,每个都会说明每个方法可以在哪些事件中使用:
|
||||
|
||||
## getServer() - 获取 Server 对象
|
||||
|
||||
获取 Swoole WebSocker Server 对象。此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server)。
|
||||
|
||||
可以使用的事件:`@OnMessageEvent()`,`@OnOpenEvent()`,`@OnCloseEvent()`,`@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
## getFrame() - 获取 WS 数据帧
|
||||
|
||||
获取 `\Swoole\Websocket\Frame` 对象,此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
|
||||
|
||||
可以使用的事件:`@OnMessageEvent()` 以及所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等,
|
||||
|
||||
## getFd() - 返回 fd 值
|
||||
|
||||
获取当前连入 Swoole 服务器的连接文件描述符 ID。返回 int。一般代表连接号,可用来绑定对应链接。
|
||||
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()`,`@OnCloseEvent()`
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
值得注意的是,由于机器人客户端和炸毛框架的连接是通过 WebSocket 进行的,而 WebSocket 是长连接,所以同一个机器人一次连接下收发消息所用的连接是同一个,所以 Fd 也是相同的。同理,炸毛框架的内部来区分多个机器人也是通过这一 Fd 进行判定的。
|
||||
|
||||
=== "代码"
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("测试fd")
|
||||
*/
|
||||
public function testfd() {
|
||||
ctx()->reply("当前机器人连接的fd是:".ctx()->getFd()",机器人QQ是:".ctx()->getRobotId());
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
^ 假设我们和连接55555的机器人的私聊
|
||||
) 测试fd
|
||||
( 当前机器人连接的fd是:1,机器人QQ是:55555
|
||||
^ 假设切到了另一个机器人(66666)的私聊
|
||||
) 测试fd
|
||||
( 当前机器人连接的fd是:2,机器人QQ是:66666
|
||||
</chat-box>
|
||||
|
||||
## getData() - 获取事件完整数据
|
||||
|
||||
返回 CQHTTP 事件上报的原始数据包,已经被解析成数组,可以直接操作。
|
||||
|
||||
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQMessage(user_id=123456)
|
||||
*/
|
||||
public function onMessage() {
|
||||
$data = ctx()->getData();
|
||||
ctx()->reply("消息类型是:" . $data["message_type"]);
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我是QQ为123456的用户,私聊发消息
|
||||
) 哈咯!!
|
||||
( 消息类型是:private
|
||||
</chat-box>
|
||||
|
||||
## getRequest() - HTTP 请求对象
|
||||
|
||||
返回 `\Swoole\Http\Request` 对象,可在 `@RequestMapping` 中使用,获取 Cookie,请求头,GET 参数什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httprequest)。
|
||||
|
||||
可以使用的事件:`@RequestMapping()`,`@OnRequestEvent()`,`@OnOpenEvent()`。
|
||||
|
||||
## getResponse() - HTTP 响应对象
|
||||
|
||||
返回 `\Swoole\Http\Response` 对象的增强版,可在 HTTP 请求相关的事件中使用,返回内容和设置 Cookie 什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httpresponse)。
|
||||
|
||||
可以使用的事件:`@RequestMapping()`,`@OnRequestEvent()`。
|
||||
|
||||
下面是使用以上两个功能的组合示例:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/ping")
|
||||
*/
|
||||
public function ping() {
|
||||
$name = ctx()->getRequest()->get["name"] ?? "unknown";
|
||||
ctx()->getResponse()->end("Hello ".$name."!");
|
||||
}
|
||||
```
|
||||
|
||||
## getConnection() - WS 连接对象
|
||||
|
||||
返回此上下文相关联的 WebSocket 连接对象。
|
||||
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。
|
||||
|
||||
## getCid() - 上下文 ID
|
||||
|
||||
返回当前上下文所绑定的协程 ID,此 ID 和 `\Co::getCid()` 返回值一样。
|
||||
|
||||
## getRobot() - 获取机器人 API 对象
|
||||
|
||||
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
|
||||
|
||||
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
```php
|
||||
ctx()->getRobot()->sendPrivateMsg(123456, "发送私聊消息");
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 正在和机器人聊天
|
||||
( 发送私聊消息
|
||||
</chat-box>
|
||||
|
||||
## getMessage() - 获取消息
|
||||
|
||||
获取 data 数据中的 `message` 消息,用于快速获取用户消息事件的消息内容。
|
||||
|
||||
可以使用的事件:`@CQCommand()`,`@CQMessage`,`@CQBefore("message")`,`@CQAfter("message")`
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
/**
|
||||
* @CQMessage(group_id=33333)
|
||||
*/
|
||||
public function groupRepeat() {
|
||||
ctx()->reply(ctx()->getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
<chat-box>
|
||||
^ 现在在群33333内,机器人已经成了复读机
|
||||
) 来世还做复读机!!!
|
||||
( 来世还做复读机!!!
|
||||
) 你不许复读!
|
||||
( 你不许复读!
|
||||
</chat-box>
|
||||
|
||||
## getUserId() - 获取用户 QQ 号
|
||||
|
||||
获取发消息的用户的 QQ 号码。
|
||||
|
||||
可以使用的事件:所有 **含有** `user_id` 上报参数的 OneBot 事件。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("whoami")
|
||||
*/
|
||||
public function whoami() {
|
||||
ctx()->reply("你是".ctx()->getUserId()); //返回:你是123456
|
||||
}
|
||||
```
|
||||
|
||||
## getGroupId() - 获取 QQ 群号
|
||||
|
||||
获取发消息来自的 QQ 群号。
|
||||
|
||||
可以使用的事件:所有含有 `group_id` 上报参数的 OneBot 事件。
|
||||
|
||||
## getMessageType() - 消息类型
|
||||
|
||||
获取消息类型,同参数 `message_type`。
|
||||
|
||||
可以使用的事件:所有 `post_type` 为 `message` 的响应事件,如 `@CQMessage`,`@CQCommand`。
|
||||
|
||||
## getRobotId() - 机器人 QQ 号
|
||||
|
||||
获取事件上报的机器人自己的 QQ 号码。
|
||||
|
||||
可以使用的事件:所有 OneBot 发来的事件:`@CQCommand()`,`@CQNotice()` 等。
|
||||
|
||||
## setMessage() - 设置消息
|
||||
|
||||
与 `getMessage()` 对应,用于更改上下文中保存的事件信息,可以用于消息变更和过滤。
|
||||
|
||||
## setUserId() - 设置用户 ID
|
||||
|
||||
与上同理,更改 `user_id`。
|
||||
|
||||
## setGroupId() - 设置群号
|
||||
|
||||
与上同理。
|
||||
|
||||
## setMessageType() - 设置类型
|
||||
|
||||
与上同理,修改消息类型。
|
||||
|
||||
## setData() - 设置数据包
|
||||
|
||||
与上同理,与 `getData()` 对应,用于更改上下文中的 `data`。
|
||||
|
||||
## getCache() - 上下文缓存
|
||||
|
||||
获取保存在上下文中的临时缓存变量。当相关联的事件结束后,数据会从内存中被释放。用于同一事件的多个函数中的信息传递。
|
||||
|
||||
- 参数:`$key`,缓存变量的键名
|
||||
- 返回:`mixed`,存入缓存的变量值。
|
||||
|
||||
```php
|
||||
$a = ctx()->getCache("block_continue");
|
||||
// 如果变量不存在,则返回 null
|
||||
```
|
||||
|
||||
## setCache() - 上下文缓存
|
||||
|
||||
与 `getCache()` 对应,是设置内容的。
|
||||
|
||||
```php
|
||||
ctx()->setCache("abc", "asdasd");
|
||||
$result = ctx()->getCache("abc"); // asdasd
|
||||
```
|
||||
|
||||
## reply() - 快速回复
|
||||
|
||||
快速回复当前用户消息内容。
|
||||
|
||||
- 参数1:`$msg`,字符串,你要回复的消息内容
|
||||
- 参数2:`$yield = false`,可选,当为 `true` 时,会协程等待后返回 **消息回复** 的结果,包括 API 状态码、消息 `message_id` 等。
|
||||
|
||||
```php
|
||||
$r = ctx()->reply("我又好了。");
|
||||
if($r["retcode"] == 0) Console::success("消息发送成功!");
|
||||
```
|
||||
|
||||
## finalReply() - 快速回复
|
||||
|
||||
快速回复用户消息,并阻止其他模块接下来继续处理此事件。
|
||||
|
||||
参数同 `reply()`。
|
||||
|
||||
## waitMessage()
|
||||
|
||||
- 参数:`waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "")`
|
||||
- 用途:等待用户输入消息
|
||||
|
||||
`$prompt` 参数为回复用户的文本内容,`$timeout` 是等待用户回复的超时时间(秒),`$timeout_prompt` 是超时后回复用户的文本。
|
||||
|
||||
这个功能可以让开发机器人的代码逻辑和实际贴合,避免回调地狱、拼接参数、上下文脱节等问题,比如下方的示例,可以仅仅用两行代码实现一个问答式的对话过程。
|
||||
|
||||
用法示例:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("自我介绍")
|
||||
*/
|
||||
function yourName(){
|
||||
$r = ctx()->waitMessage("你叫啥名字呀?", 600, "你都10分钟不理我了,嘤嘤嘤");
|
||||
ctx()->finalReply("好的,可爱的机器人记住你叫 ".$r." 啦!以后多聊天哦!");
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 自我介绍
|
||||
( 你叫啥名字呀?
|
||||
) jerry
|
||||
( 好的,可爱的机器人记住你叫 jerry 啦!以后多聊天哦!
|
||||
) 自我介绍
|
||||
( 你叫啥名字呀?
|
||||
^ 10分钟没理机器人
|
||||
( 你都10分钟不理我了,嘤嘤嘤
|
||||
</chat-box>
|
||||
|
||||
## getArgs()
|
||||
|
||||
TODO:还没写到这里,下次更新,今晚太困了。
|
||||
|
||||
## copy()
|
||||
|
||||
t
|
||||
|
||||
## getOption() - 获取匹配参数内容
|
||||
|
||||
|
||||
424
docs/component/cqcode.md
Normal file
424
docs/component/cqcode.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# CQ 码(多媒体消息)
|
||||
|
||||
消息中的多媒体内容使用 CQ 码来表示,形如 `[CQ:face,id=178]`。其中,`[CQ:]` 是固定格式;`face` 是「功能名」,除了 `face` 还有许多不同的功能名;`id=178` 是「参数」,某些功能不需要参数,而另一些需要多个参数,当有多个参数时,参数间使用逗号分隔。
|
||||
|
||||
## 格式
|
||||
|
||||
一些 CQ 码的例子如下:
|
||||
|
||||
```
|
||||
[CQ:shake]
|
||||
[CQ:face,id=178]
|
||||
[CQ:share,title=标题,url=http://baidu.com]
|
||||
```
|
||||
|
||||
更多 CQ 码功能请参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md)。
|
||||
|
||||
!!! warning "注意"
|
||||
|
||||
CQ 码中不应有多余的空格,例如不应该使用 `[CQ:face, id=178]`。
|
||||
|
||||
CQ 码的参数值可以包含空格、换行、除 `[],&` 之外的特殊符号等。在解析时,应直接取 `[CQ:` 后、第一个 `,` 或 `]` 前的部分为功能名,第一个 `,` 之后到 `]` 之间的部分为参数,按 `,` 分割后,每个部分第一个 `=` 前的内容为参数名,之后的部分为参数值。例如 `[CQ:share,title=标题中有=等号,url=http://baidu.com]` 中,功能名为 `share`,`title` 参数值为 `标题中有=等号`,`url` 参数值为 `http://baidu.com`。
|
||||
|
||||
## 转义
|
||||
|
||||
CQ 码中包含一些特殊字符:`[`、`]`、`,` 等,而 CQ 码又是可能混杂在纯文本内容之中的,因此消息中的纯文本内容需要对特殊字符进行转义,以避免歧义。具体的转义规则如下:
|
||||
|
||||
| 转义前 | 转义后 |
|
||||
| ------ | ------- |
|
||||
| `&` | `&` |
|
||||
| `[` | `[` |
|
||||
| `]` | `]` |
|
||||
|
||||
另一方面,CQ 码内部的参数值也可能出现特殊字符,也是需要转义的。由于 `,`(半角逗号)在 CQ 码中用于分隔参数,因此除了上面的转义规则,还需要对 `,` 进行转义,如下:
|
||||
|
||||
| 转义前 | 转义后 |
|
||||
| ------ | ------- |
|
||||
| `&` | `&` |
|
||||
| `[` | `[` |
|
||||
| `]` | `]` |
|
||||
| `,` | `,` |
|
||||
|
||||
例如,一个链接分享消息的 CQ 码可能如下:
|
||||
|
||||
```
|
||||
[CQ:share,title=震惊,小伙睡觉前居然...,url=http://baidu.com/?a=1&b=2]
|
||||
```
|
||||
|
||||
## 封装调用
|
||||
|
||||
框架提供了 CQ 码的封装,你可以在任何位置使用封装好的 CQ 码生成器。
|
||||
|
||||
生成器是一个静态类,里面的方法全部是静态调用,命名空间是:`ZM\API\CQ`。
|
||||
|
||||
例如,给用户发送图片这样写就好啦!只需要将添加图片的地方拼到回复用户的字符串里。如果只发图片,整个字符串里只能有 CQ 码。
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\API\CQ;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
class Hello {
|
||||
/**
|
||||
* @CQCommand("发送图片")
|
||||
*/
|
||||
public function msgRecv() {
|
||||
return CQ::image("https://zhamao.xin/file/hello.jpg");
|
||||
// 相当于返回:"[CQ:image,file=https://zhamao.xin/file/hello.jpg]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
效果
|
||||
|
||||
<chat-box>
|
||||
) 发送图片
|
||||
[ https://zhamao.xin/file/hello.jpg
|
||||
</chat-box>
|
||||
|
||||
## CQ 码列表
|
||||
|
||||
### CQ::face() - 发送 QQ 表情
|
||||
|
||||
发送 QQ 原生表情。
|
||||
|
||||
定义:`CQ::face($id)`
|
||||
|
||||
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [炸毛框架 1.x 版本文档](https://docs-v1.zhamao.xin/face_list.html)。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("打盹")
|
||||
*/
|
||||
public function faceTest() {
|
||||
ctx()->reply("正在打盹...");
|
||||
ctx()->reply(CQ::face(8));
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 打盹
|
||||
( 正在打盹...
|
||||
[ https://docs-v1.zhamao.xin/face/8.gif
|
||||
</chat-box>
|
||||
|
||||
!!! note "提示"
|
||||
对于不断更新的 QQ 版本下,可能会持续扩充新的 QQ 表情,如果上表没有新的表情的话,也可以使用消息接收的方式,让机器人收到表情后解析出来对应的 id 然后再发送。
|
||||
|
||||
|
||||
### CQ::image() - 发送图片
|
||||
|
||||
发送图片。
|
||||
|
||||
定义:`image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 默认值 | 说明 |
|
||||
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
|
||||
| `file` | ✓ | ✓ | 必填 | 图片文件名 |
|
||||
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
|
||||
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
|
||||
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
|
||||
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
|
||||
|
||||
发送时,`file` 参数除了支持使用收到的图片文件名直接发送外,还支持:
|
||||
|
||||
- 绝对路径,例如 `file:///root/imagetest/1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089)
|
||||
- 网络 URL,例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg`
|
||||
- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
|
||||
|
||||
### CQ::record() - 发送语音
|
||||
|
||||
发送语音消息。
|
||||
|
||||
定义:`CQ::record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 默认值 | 说明 |
|
||||
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
|
||||
| `file` | ✓ | ✓ | 必填 | 音频文件名 |
|
||||
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
|
||||
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
|
||||
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
|
||||
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
|
||||
|
||||
发送时,`file` 参数除了支持使用收到的语音文件名直接发送外,还支持其它形式,参考上方发送图片。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("说你好")
|
||||
*/
|
||||
public function say() {
|
||||
ctx()->reply(CQ::record("https://zhamao.xin/file/hello.mp3"));
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 说你好
|
||||
( [语音消息,点击收听] 2'' )))
|
||||
</chat-box>
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::at() - 群里@某人或全体
|
||||
|
||||
在群里 at 某个人或全体成员(全体成员需要有管理员权限)。
|
||||
|
||||
定义:`CQ::at($qq)`
|
||||
|
||||
参数:`$qq` 参数必填,如果填的是 QQ 号,则是单独 at 某人,如果是 `all`,则是 at 全体成员。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("at测试")
|
||||
*/
|
||||
public function atTest() {
|
||||
ctx()->reply(CQ::at(627577391)." 你好啊!");
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) at测试
|
||||
( @鲸鱼 你好啊!
|
||||
</chat-box>
|
||||
|
||||
### CQ::video() - 发送短视频
|
||||
|
||||
发送短视频。
|
||||
|
||||
定义:`CQ::video($file, $cache = true, $proxy = true, $timeout = -1)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 默认值 | 说明 |
|
||||
| --------- | ---- | ---- | ------ | ------------------------------------------------------------ |
|
||||
| `file` | ✓ | ✓ | 必填 | 短视频文件名 |
|
||||
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
|
||||
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
|
||||
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
|
||||
|
||||
发送时,`file` 参数除了支持使用收到的视频文件名直接发送外,还支持其它形式,参考上方发送图片。
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::rps() - 猜拳
|
||||
|
||||
定义:`CQ::rps()`
|
||||
|
||||
用法:`CQ::rps()`
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::dice() - 掷骰子
|
||||
|
||||
定义:`CQ::dice()`
|
||||
|
||||
用法:`CQ::dice()`
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::shake() - 窗口抖动
|
||||
|
||||
定义:`CQ::shake()`
|
||||
|
||||
用法:`CQ::shake()`
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::poke() - 戳一戳
|
||||
|
||||
发送戳一戳。
|
||||
|
||||
定义:`CQ::poke($type, $id, $name = "")`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| ------ | ---- | ---- | ------------------------------------------------------------ | ------ |
|
||||
| `type` | ✓ | ✓ | 见 [Mirai 的 PokeMessage 类](https://github.com/mamoe/mirai/blob/f5eefae7ecee84d18a66afce3f89b89fe1584b78/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt#L49) | 类型 |
|
||||
| `id` | ✓ | ✓ | 同上 | ID |
|
||||
| `name` | ✓ | | 同上 | 表情名 |
|
||||
|
||||
例子:`CQ::poke(6,-1)`
|
||||
|
||||
效果:放大招
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
### CQ::anonymous() - 匿名发消息
|
||||
|
||||
匿名发消息。需要在允许匿名发消息的群里发。
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
当收到匿名消息时,需要通过 [消息事件的群消息](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#群消息) 的 `anonymous` 字段判断。
|
||||
|
||||
定义:`CQ::anonymous($ignore = 1)`
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("匿名测试")
|
||||
*/
|
||||
public function anonymousTest() {
|
||||
ctx()->reply(CQ::anonymous()."匿名测试");
|
||||
}
|
||||
```
|
||||
|
||||
### CQ::share() - 链接分享
|
||||
|
||||
发送链接分享卡片,可自定义内容。
|
||||
|
||||
定义:`CQ::share($url, $title, $content = null, $image = null)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| --------- | ---- | ---- | -------- | -------------- |
|
||||
| `url` | ✓ | ✓ | - | URL |
|
||||
| `title` | ✓ | ✓ | - | 标题 |
|
||||
| `content` | ✓ | ✓ | - | 可选,内容描述 |
|
||||
| `image` | ✓ | ✓ | - | 可选,图片 URL |
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("链接分享测试")
|
||||
*/
|
||||
public function shareTest() {
|
||||
ctx()->reply(CQ::share("https://baidu.com", "UC忽悠部", "震惊!我市一男子在光天化日之下..."));
|
||||
}
|
||||
```
|
||||
|
||||
### CQ::contact() - 推荐好友
|
||||
|
||||
发送推荐好友的卡片。
|
||||
|
||||
定义:`CQ::contact($type, $id)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| ------ | ---- | ---- | ------------- | ---------------------- |
|
||||
| `type` | ✓ | ✓ | `qq`,`group` | 推荐好友或群 |
|
||||
| `id` | ✓ | ✓ | - | 被推荐人的 QQ 号或群号 |
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("我的名片")
|
||||
*/
|
||||
public function myCard() {
|
||||
ctx()->reply(CQ::contact("qq", ctx()->getUserId()));
|
||||
}
|
||||
```
|
||||
|
||||
### CQ::location() - 发送位置
|
||||
|
||||
发送位置,基于经纬度坐标发的。
|
||||
|
||||
定义:`CQ::location($lat, $lon, $title = "", $content = "")`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| --------- | ---- | ---- | -------- | -------------- |
|
||||
| `lat` | ✓ | ✓ | - | 纬度 |
|
||||
| `lon` | ✓ | ✓ | - | 经度 |
|
||||
| `title` | ✓ | ✓ | - | 可选,标题 |
|
||||
| `content` | ✓ | ✓ | - | 可选,内容描述 |
|
||||
|
||||
### CQ::music() - 音乐分享
|
||||
|
||||
分享音乐,通过卡片。
|
||||
|
||||
发送音乐分享卡片。此 CQ 码如果伴随着其他文字,则文字内容会被丢弃。
|
||||
|
||||
定义:`CQ::music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null)`
|
||||
|
||||
- `$type`: 发送类型
|
||||
- `$id_or_url`: 音乐的 id 或 音乐卡片点进去打开的链接
|
||||
- `$audio`: 音频文件的 HTTP 地址
|
||||
- `$title`: 音乐卡片的标题,建议 12 字以内
|
||||
- `$content`: 音乐卡片的简介内容(可选)
|
||||
- `$image`: 音乐卡片的图片的链接地址(可选)
|
||||
|
||||
如果 `$type` 参数为 `qq` 或 `163` 或 `xiami`,则必须且只和第二个参数 `$id_or_url` 配合使用。这三个为内置分享,需要先通过搜索功能获取对应平台歌曲的 id 后使用。
|
||||
|
||||
如果 `$type` 参数为 `custom`,则表明此音乐卡片为用户自定义,你可以根据自己的需要自定义卡片内容和音频。此时必须填写 `$id_or_url`, `$audio`, `$title` 三个参数。
|
||||
|
||||
```php
|
||||
ctx()->reply(CQ::music("163", "730806")); //一首我喜欢的歌
|
||||
// 以内置的发送类型发送音乐卡片,我这里挑了网易云音乐的一首歌。
|
||||
|
||||
ctx()->reply("custom", "https://baidu.com/", "https://zhamao.xin/file/hello.mp3", "我是Siri说出来的Hello", "不服来打我呀!", "https://zhamao.xin/file/hello.jpg");
|
||||
// 自定义整个卡片的每个内容
|
||||
```
|
||||
|
||||
### CQ::forward() - 合并转发
|
||||
|
||||
合并转发消息。
|
||||
|
||||
定义:`CQ::forward($id)`
|
||||
|
||||
参数
|
||||
|
||||
```
|
||||
[CQ:forward,id=123456]
|
||||
```
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| ------ | ---- | ---- | -------- | ------------------------------------------------------------ |
|
||||
| `id` | ✓ | | 必填 | 合并转发 ID,需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取具体内容 |
|
||||
|
||||
### CQ::node() - 合并转发自定义节点
|
||||
|
||||
接收时,此消息段不会直接出现在消息事件的 `message` 中,需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取。发送时,通过获取回来的 API 节点信息进行发送。
|
||||
|
||||
定义:`CQ::node($user_id, $nickname, $content)`
|
||||
|
||||
参数
|
||||
|
||||
| 参数名 | 收 | 发 | 可能的值 | 说明 |
|
||||
| ---------- | ---- | ---- | -------- | ------------------------------------------------------------ |
|
||||
| `user_id` | ✓ | ✓ | - | 发送者 QQ 号 |
|
||||
| `nickname` | ✓ | ✓ | - | 发送者昵称 |
|
||||
| `content` | ✓ | ✓ | - | 消息内容,支持发送消息时的 `message` 数据类型,见 [API 的参数](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/#参数) |
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("node测试")
|
||||
*/
|
||||
public function nodeTest() {
|
||||
ctx()->reply(CQ::node(123456, "Jack", "[CQ:face,id=123]哈喽~"));
|
||||
}
|
||||
```
|
||||
|
||||
### CQ::xml() - XML 消息
|
||||
|
||||
发送 QQ 兼容的 XML 多媒体消息。
|
||||
|
||||
定义:`CQ::xml($data)`
|
||||
|
||||
参数:`$data` 为 xml 字符串
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("xml测试")
|
||||
*/
|
||||
public function xmlTest() {
|
||||
ctx()->reply(CQ::xml("<?xml ..."));
|
||||
}
|
||||
```
|
||||
|
||||
### CQ::json() - JSON 消息
|
||||
|
||||
发送 QQ 兼容的 JSON 多媒体消息。
|
||||
|
||||
定义:`CQ::json($data)`
|
||||
|
||||
参数同上,内含 JSON 字符串即可。
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
|
||||
|
||||
3
docs/component/index.md
Normal file
3
docs/component/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 框架组件
|
||||
|
||||
这里列举了框架内的你可能会用到的常用组件。
|
||||
790
docs/component/robot-api.md
Normal file
790
docs/component/robot-api.md
Normal file
@@ -0,0 +1,790 @@
|
||||
# 机器人 API(ZMRobot)
|
||||
|
||||
ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
|
||||
|
||||
| 属性项 | 属性值 | 备注 |
|
||||
| -------- | ---------------- | ------------------------------ |
|
||||
| 名称 | ZMRobot | |
|
||||
| 类型 | 实例化类 | `$r = new ZMRobot($conn)` |
|
||||
| 命名空间 | `ZM\API\ZMRobot` | 使用前先 `use ZM\API\ZMRobot;` |
|
||||
|
||||
## 属性
|
||||
|
||||
对象属性方法是对 API 的调整,例如是否以 `_async`、`_rate_limited` 后缀发送 API、设置协程返回还是异步返回结果等。
|
||||
|
||||
### ZMRobot::API_NORMAL
|
||||
|
||||
以默认(无后缀)方式请求 API。
|
||||
|
||||
### ZMRobot::API_ASYNC
|
||||
|
||||
以后缀 `_async` 方式异步请求 API。
|
||||
|
||||
### ZMRobot::API_RATE_LIMITED
|
||||
|
||||
以后缀 `_rate_limited` 方式请求 API。
|
||||
|
||||
## 方法
|
||||
|
||||
### setPrefix()
|
||||
|
||||
设置后缀。目前支持 `_async`、`_rate_limited`。
|
||||
|
||||
- **prefix**: `int` `默认:API_NORMAL`,可选 `ZMRobot::API_NORMAL`、`ZMRobot::API_ASYNC`、`ZMRobot::API_RATE_LIMITED`
|
||||
|
||||
设置后缀后,请求的 API 会发生变化。例如发送私聊消息:`sendPrivateMsg()`,请求的 API 为 `send_private_msg_async`,详见 [OneBot 文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/README.md)。
|
||||
|
||||
### setCallback()
|
||||
|
||||
设置 API 结果返回方式。默认为 true,就是直接通过框架处理后接收回包直接返回给结果。如果为 false,则 API 请求后只返回是否成功推送出 WS 数据包。
|
||||
|
||||
### getSelfId()
|
||||
|
||||
获取当前对象的机器人 QQ 或 OneBot 实例的 ID。
|
||||
|
||||
```php
|
||||
$bot = ZMRobot::get(123456);
|
||||
echo $bot->getSelfId(); //123456
|
||||
```
|
||||
|
||||
### ZMRobot::get()
|
||||
|
||||
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 ZMRobot 对象。
|
||||
|
||||
参数:`$robot_id`,必填。
|
||||
|
||||
```php
|
||||
$r = ZMRobot::get(123456);
|
||||
$r->sendPrivateMsg(55555, "hello");
|
||||
```
|
||||
|
||||
### ZMRobot::getRandom()
|
||||
|
||||
静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。
|
||||
|
||||
如果框架没有连接到任何机器人实例,则会抛出一个异常:`ZM\Exception\RobotNotFoundException`。
|
||||
|
||||
```php
|
||||
try {
|
||||
$bot = ZMRobot::getRandom();
|
||||
$bot->sendPrivateMsg(55555, "foo");
|
||||
} catch (\ZM\Exception\RobotNotFoundException $e) {
|
||||
echo "还没有机器人连接到框架!\n";
|
||||
}
|
||||
```
|
||||
|
||||
### ZMRobot::getAllRobot()
|
||||
|
||||
获取所有连接到框架的机器人的 ZMRobot 对象。
|
||||
|
||||
返回值:`ZMRobot[]`。
|
||||
|
||||
```php
|
||||
$all = ZMRobot::getAllRobot();
|
||||
foreach($all as $v) {
|
||||
$v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!");
|
||||
}
|
||||
```
|
||||
|
||||
### __construct()
|
||||
|
||||
构造方法。
|
||||
|
||||
参数:`$connection`:炸毛框架内部的连接对象,必填参数。
|
||||
|
||||
```php
|
||||
//从上下文获取 Websocket 连接对象
|
||||
$conn = ctx()->getConnection();
|
||||
$bot = new ZMRobot($conn);
|
||||
```
|
||||
|
||||
## 返回结果处理
|
||||
|
||||
因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP),所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID,以供我们核查消息和后续撤回等操作需要。
|
||||
|
||||
```php
|
||||
$bot = ZMRobot::get("123456"); // 机器人QQ号
|
||||
$obj = $bot->sendGroupMsg("234567", "你好");
|
||||
echo json_encode($obj, 128|256);
|
||||
```
|
||||
|
||||
```json
|
||||
// 输出结果
|
||||
{
|
||||
"status": "ok",
|
||||
"retcode": 0,
|
||||
"data": {
|
||||
"message_id": 1243
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如上,`$obj` 就是我们的回包内容,我们通过调用 `sendPrivateMsg()` 这个 API 后,会拿到机器人发送此条消息的消息 ID,然后可以通过它来进行其他操作(例如撤回)。
|
||||
|
||||
```php
|
||||
$result = $bot->deleteMsg($obj["data"]["message_id"]);
|
||||
vardump($result["retcode"]); //如果成功撤回,输出 int(0)
|
||||
```
|
||||
|
||||
### 状态码和 Data
|
||||
|
||||
状态码一般情况成功都是 0 或者 200,在过去,炸毛框架兼容 CQHTTP 插件时,错误码的标准按照 CYKU 给出的标准编写,不同的 OneBot 标准的实现,可能有不同的数值,需要根据你对接的机器人客户端进行适配。
|
||||
|
||||
结果中返回的 `data` 字段根据下方不同 API 的调用而不同,具体查看每个 API 写明的 `响应数据` 表格。
|
||||
|
||||
### response 表
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ---------- | ----------------------- |
|
||||
| `status` | String | "ok" | 状态码说明 |
|
||||
| `retcode` | number | 0 | 返回状态码 |
|
||||
| `data` | array | 见 data 表 | 根据不同的 API 返回不同 |
|
||||
|
||||
## 机器人 API 方法
|
||||
|
||||
### sendPrivateMsg()
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------- | -------- | ------- | ------------------------------------------------------------ |
|
||||
| `user_id` | number | - | 对方 QQ 号 |
|
||||
| `message` | message | - | 要发送的内容 |
|
||||
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ------- |
|
||||
| `message_id` | number (int32) | 消息 ID |
|
||||
|
||||
例子
|
||||
|
||||
=== "代码"
|
||||
|
||||
```php
|
||||
$bot = ZMRobot::get(123456); // 123456是你的机器人QQ
|
||||
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
( 你好啊!你好你好!
|
||||
</chat-box>
|
||||
|
||||
|
||||
### sendGroupMsg()
|
||||
|
||||
发送群组消息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------- | -------- | ------- | ------------------------------------------------------------ |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `message` | message | - | 要发送的内容 |
|
||||
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ------- |
|
||||
| `message_id` | number (int32) | 消息 ID |
|
||||
|
||||
### sendMsg()
|
||||
|
||||
发送消息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| -------------- | -------- | ------- | ------------------------------------------------------------ |
|
||||
| `message_type` | string | - | 消息类型,支持 `private`、`group`、`discuss`,分别对应私聊、群组、讨论组,如不传入,则根据传入的 `*_id` 参数判断 |
|
||||
| `target_id` | number | - | 目标号码,如 QQ 号,群号,讨论组号 |
|
||||
| `message` | message | - | 要发送的内容 |
|
||||
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ------- |
|
||||
| `message_id` | number (int32) | 消息 ID |
|
||||
|
||||
### deleteMsg()
|
||||
|
||||
撤回消息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------ | -------------- | ------ | ------- |
|
||||
| `message_id` | number (int32) | - | 消息 ID |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### getMsg()
|
||||
|
||||
获取消息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ------ | -------------------------------- |
|
||||
| `message_id` | number | - | 消息 ID |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| -------------- | -------------- | ------------------------------------------------------------ |
|
||||
| `time` | number (int32) | 发送时间 |
|
||||
| `message_type` | string | 消息类型,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
|
||||
| `message_id` | number (int32) | 消息 ID |
|
||||
| `real_id` | number (int32) | 消息真实 ID |
|
||||
| `sender` | object | 发送人信息,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
|
||||
| `message` | message | 消息内容 |
|
||||
|
||||
### getForwardMsg()
|
||||
|
||||
获取合并转发消息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------ | -------- | ----------- |
|
||||
| `id` | string | 合并转发 ID |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
| --------- | ------- | ------------------------------------------------------------ |
|
||||
| `message` | message | 消息内容,使用 [消息的数组格式](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md) 表示,数组中的消息段全部为 [`node` 消息段](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#合并转发自定义节点) |
|
||||
|
||||
### sendLike()
|
||||
|
||||
发送好友赞。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ------ | -------------------------------- |
|
||||
| `user_id` | number | - | 对方 QQ 号 |
|
||||
| `times` | number | 1 | 赞的次数,每个好友每天最多 10 次 |
|
||||
|
||||
响应数据
|
||||
|
||||
无
|
||||
|
||||
### setGroupKick()
|
||||
|
||||
群组踢人。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| -------------------- | -------- | ------- | ------------------ |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | 要踢的 QQ 号 |
|
||||
| `reject_add_request` | boolean | `false` | 拒绝此人的加群请求 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupBan()
|
||||
|
||||
群组单人禁言。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | --------- | -------------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | 要禁言的 QQ 号 |
|
||||
| `duration` | number | `30 * 60` | 禁言时长,单位秒,0 表示取消禁言 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupAnonymousBan()
|
||||
|
||||
群组匿名用户禁言。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------------- | ---------------- | --------- | ------------------------------------------------------------ |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `anonymous_or_flag` | object 或 string | - | 要禁言的匿名用户对象(群消息上报的 `anonymous` 字段)或用户的 flag |
|
||||
| `duration` | number | `30 * 60` | 禁言时长,单位秒,无法取消匿名用户禁言 |
|
||||
|
||||
上面的 `anonymous_or_flag` 两者任选其一传入即可。
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupWholeBan()
|
||||
|
||||
群组全员禁言
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | -------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `enable` | boolean | `true` | 是否禁言 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupAdmin()
|
||||
|
||||
群组设置管理员
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | ------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | 要设置管理员的 QQ 号 |
|
||||
| `enable` | boolean | `true` | true 为设置,false 为取消 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupAnonymous()
|
||||
|
||||
群组匿名
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | ---------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `enable` | boolean | `true` | 是否允许匿名聊天 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupCard()
|
||||
|
||||
设置群名片(群备注)
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | ---------------------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | 要设置的 QQ 号 |
|
||||
| `card` | string | 空 | 群名片内容,不填或空字符串表示删除群名片 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupName()
|
||||
|
||||
设置群名。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ------ |
|
||||
| `group_id` | number (int64) | 群号 |
|
||||
| `group_name` | string | 新群名 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupLeave()
|
||||
|
||||
退出群组
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------ | -------- | ------- | -------------------------------------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `is_dismiss` | boolean | `false` | 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupSpecialTitle()
|
||||
|
||||
设置群组专属头衔
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| --------------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | 要设置的 QQ 号 |
|
||||
| `special_title` | string | 空 | 专属头衔,不填或空字符串表示删除专属头衔 |
|
||||
| `duration` | number | `-1` | 专属头衔有效期,单位秒,-1 表示永久,不过此项似乎没有效果,可能是只有某些特殊的时间长度有效,有待测试 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setFriendAddRequest()
|
||||
|
||||
处理加好友请求
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| --------- | -------- | ------ | ----------------------------------------- |
|
||||
| `flag` | string | - | 加好友请求的 flag(需从上报的数据中获得) |
|
||||
| `approve` | boolean | `true` | 是否同意请求 |
|
||||
| `remark` | string | 空 | 添加后的好友备注(仅在同意时有效) |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### setGroupAddRequest()
|
||||
|
||||
处理加群请求 / 邀请
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `flag` | string | - | 加群请求的 flag(需从上报的数据中获得) |
|
||||
| `sub_type` | string | - | `add` 或 `invite`,请求类型(需要和上报消息中的 `sub_type` 字段相符) |
|
||||
| `approve` | boolean | `true` | 是否同意请求/邀请 |
|
||||
| `reason` | string | 空 | 拒绝理由(仅在拒绝时有效) |
|
||||
|
||||
响应数据无
|
||||
|
||||
### getLoginInfo()
|
||||
|
||||
获取登录号信息
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ---------- | -------------- | ------- |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | QQ 昵称 |
|
||||
|
||||
### getStrangerInfo()
|
||||
|
||||
获取陌生人信息
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------- | ---------------------------------------------------- |
|
||||
| `user_id` | number | - | QQ 号 |
|
||||
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ---------- | -------------- | ------------------------------------- |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | 昵称 |
|
||||
| `sex` | string | 性别,`male` 或 `female` 或 `unknown` |
|
||||
| `age` | number (int32) | 年龄 |
|
||||
|
||||
### getFriendList()
|
||||
|
||||
获取好友列表
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
响应内容为 JSON 数组,每个元素如下:
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ---------- | -------------- | ------ |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | 昵称 |
|
||||
| `remark` | string | 备注名 |
|
||||
|
||||
### getGroupInfo()
|
||||
|
||||
获取群信息
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------- | ---------------------------------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------------ | -------------- | -------------------- |
|
||||
| `group_id` | number (int64) | 群号 |
|
||||
| `group_name` | string | 群名称 |
|
||||
| `member_count` | number (int32) | 成员数 |
|
||||
| `max_member_count` | number (int32) | 最大成员数(群容量) |
|
||||
|
||||
### getGroupList()
|
||||
|
||||
获取群列表
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
响应内容为 JSON 数组,每个元素如下:
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ------ |
|
||||
| `group_id` | number (int64) | 群号 |
|
||||
| `group_name` | string | 群名称 |
|
||||
|
||||
### getGroupMemberInfo()
|
||||
|
||||
获取群成员信息
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------- | ---------------------------------------------------- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
| `user_id` | number | - | QQ 号 |
|
||||
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------------- | -------------- | ------------------------------------- |
|
||||
| `group_id` | number (int64) | 群号 |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | 昵称 |
|
||||
| `card` | string | 群名片/备注 |
|
||||
| `sex` | string | 性别,`male` 或 `female` 或 `unknown` |
|
||||
| `age` | number (int32) | 年龄 |
|
||||
| `area` | string | 地区 |
|
||||
| `join_time` | number (int32) | 加群时间戳 |
|
||||
| `last_sent_time` | number (int32) | 最后发言时间戳 |
|
||||
| `level` | string | 成员等级 |
|
||||
| `role` | string | 角色,`owner` 或 `admin` 或 `member` |
|
||||
| `unfriendly` | boolean | 是否不良记录成员 |
|
||||
| `title` | string | 专属头衔 |
|
||||
| `title_expire_time` | number (int32) | 专属头衔过期时间戳 |
|
||||
| `card_changeable` | boolean | 是否允许修改群名片 |
|
||||
|
||||
### getGroupMemberList()
|
||||
|
||||
获取群成员列表
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------- | ------ | ---- |
|
||||
| `group_id` | number | - | 群号 |
|
||||
|
||||
响应数据
|
||||
|
||||
响应内容为 JSON 数组,每个元素的内容和上面的 `/get_group_member_info` 接口相同,但对于同一个群组的同一个成员,获取列表时和获取单独的成员信息时,某些字段可能有所不同,例如 `area`、`title` 等字段在获取列表时无法获得,具体应以单独的成员信息为准。
|
||||
|
||||
### getGroupHonorInfo()
|
||||
|
||||
获取群荣誉信息。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ---------- | -------------- | ------ | ------------------------------------------------------------ |
|
||||
| `group_id` | number (int64) | - | 群号 |
|
||||
| `type` | string | - | 要获取的群荣誉类型,可传入 `talkative` `performer` `legend` `strong_newbie` `emotion` 以分别获取单个类型的群荣誉数据,或传入 `all` 获取所有数据 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| -------------------- | -------------- | ---------------------------------------------------------- |
|
||||
| `group_id` | number (int64) | 群号 |
|
||||
| `current_talkative` | object | 当前龙王,仅 `type` 为 `talkative` 或 `all` 时有数据 |
|
||||
| `talkative_list` | array | 历史龙王,仅 `type` 为 `talkative` 或 `all` 时有数据 |
|
||||
| `performer_list` | array | 群聊之火,仅 `type` 为 `performer` 或 `all` 时有数据 |
|
||||
| `legend_list` | array | 群聊炽焰,仅 `type` 为 `legend` 或 `all` 时有数据 |
|
||||
| `strong_newbie_list` | array | 冒尖小春笋,仅 `type` 为 `strong_newbie` 或 `all` 时有数据 |
|
||||
| `emotion_list` | array | 快乐之源,仅 `type` 为 `emotion` 或 `all` 时有数据 |
|
||||
|
||||
其中 `current_talkative` 字段的内容如下:
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ----------- | -------------- | -------- |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | 昵称 |
|
||||
| `avatar` | string | 头像 URL |
|
||||
| `day_count` | number (int32) | 持续天数 |
|
||||
|
||||
其它各 `*_list` 的每个元素是一个 JSON 对象,内容如下:
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------- | -------------- | -------- |
|
||||
| `user_id` | number (int64) | QQ 号 |
|
||||
| `nickname` | string | 昵称 |
|
||||
| `avatar` | string | 头像 URL |
|
||||
| `description` | string | 荣誉描述 |
|
||||
|
||||
### getCookies()
|
||||
|
||||
获取 Cookies。
|
||||
|
||||
!!! warning "注意"
|
||||
|
||||
目前开源的 mirai 为底层的机器人客户端均不支持获取 Cookies 和 CSRF Token,包括 go-cqhttp。
|
||||
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| -------- | -------- | ------ | ----------------------- |
|
||||
| `domain` | string | 空 | 需要获取 cookies 的域名 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| --------- | -------- | ------- |
|
||||
| `cookies` | string | Cookies |
|
||||
|
||||
### getCsrfToken()
|
||||
|
||||
获取 CSRF Token
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------- | -------------- | ---------- |
|
||||
| `token` | number (int32) | CSRF Token |
|
||||
|
||||
### getCredentials()
|
||||
|
||||
获取 QQ 相关接口凭证,即上面两个合并。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| -------- | -------- | ------ | ----------------------- |
|
||||
| `domain` | string | 空 | 需要获取 cookies 的域名 |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------ | -------------- | ---------- |
|
||||
| `cookies` | string | Cookies |
|
||||
| `csrf_token` | number (int32) | CSRF Token |
|
||||
|
||||
### getRecord()
|
||||
|
||||
获取语音。其实并不是真的获取语音,而是转换语音到指定的格式。
|
||||
|
||||
> **提示**:要使用此接口,通常需要安装 ffmpeg,请参考 OneBot 实现的相关说明。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------------ | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `file` | string | - | 收到的语音文件名(CQ 码的 `file` 参数),如 `0B38145AA44505000B38145AA4450500.silk` |
|
||||
| `out_format` | string | - | 要转换到的格式,目前支持 `mp3`、`amr`、`wma`、`m4a`、`spx`、`ogg`、`wav`、`flac` |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------ | -------- | ------------------------------------------------------------ |
|
||||
| `file` | string | 转换后的语音文件路径,如 `/home/somebody/cqhttp/data/record/0B38145AA44505000B38145AA4450500.mp3` |
|
||||
|
||||
### getImage()
|
||||
|
||||
获取图片。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------ | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `file` | string | - | 收到的图片文件名(CQ 码的 `file` 参数),如 `6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------ | -------- | ------------------------------------------------------------ |
|
||||
| `file` | string | 下载后的图片文件路径,如 `/home/somebody/cqhttp/data/image/6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
|
||||
|
||||
### canSendImage()
|
||||
|
||||
检查是否可以发送图片。
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------ | -------- | ------ |
|
||||
| `yes` | boolean | 是或否 |
|
||||
|
||||
### canSendRecord()
|
||||
|
||||
检查是否可以发送语音,返回同上。
|
||||
|
||||
### getStatus()
|
||||
|
||||
获取插件运行状态。
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| -------- | -------- | -------------------------------------------------------- |
|
||||
| `online` | boolean | 当前 QQ 在线,`null` 表示无法查询到在线状态 |
|
||||
| `good` | boolean | 状态符合预期,意味着各模块正常运行、功能正常,且 QQ 在线 |
|
||||
| ...... | - | OneBot 实例自行添加的其他内容 |
|
||||
|
||||
通常情况下建议只使用 `online` 和 `good` 这两个字段来判断运行状态,因为根据 OneBot 实现的不同,其它字段可能完全不同。
|
||||
|
||||
### getVersionInfo()
|
||||
|
||||
获取版本信息
|
||||
|
||||
响应数据
|
||||
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
| ------------------ | -------- | ----------------------------- |
|
||||
| `app_name` | string | 应用标识,如 `mirai-native` |
|
||||
| `app_version` | string | 应用版本,如 `1.2.3` |
|
||||
| `protocol_version` | string | OneBot 标准版本,如 `v11` |
|
||||
| …… | - | OneBot 实现自行添加的其它内容 |
|
||||
|
||||
### setRestartPlugin()
|
||||
|
||||
重启 OneBot 客户端
|
||||
|
||||
由于重启 OneBot 实现同时需要重启 API 服务,这意味着当前的 API 请求会被中断,因此需要异步地重启,接口返回的 `status` 是 `async`。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 | 说明 |
|
||||
| ------- | -------- | ------ | ------------------------------------------------------------ |
|
||||
| `delay` | number | `0` | 要延迟的毫秒数,如果默认情况下无法重启,可以尝试设置延迟为 2000 左右 |
|
||||
|
||||
响应数据:无
|
||||
|
||||
### cleanCache()
|
||||
|
||||
清理 OneBot 客户端的缓存。
|
||||
|
||||
参数:无
|
||||
|
||||
响应数据:无
|
||||
|
||||
### callExtendedAPI() (扩充 API)
|
||||
|
||||
用来调用 OneBot 标准之外扩展出来的自定义 API。
|
||||
|
||||
使用不同 OneBot 客户端时,可能有一些 API 不在上方的 OneBot 标准里,这时可以使用此方法进行额外调用。
|
||||
|
||||
参数
|
||||
|
||||
| 字段名 | 数据类型 | 默认值 |
|
||||
| -------- | -------- | ------ |
|
||||
| `action` | string | 必填 |
|
||||
| `params` | array | `[]` |
|
||||
|
||||
例子
|
||||
|
||||
```php
|
||||
$result = $bot->callExtendedAPI("get_group_root_files", ["group_id" => 123456]);
|
||||
//这里以 go-cqhttp 扩展的一个获取群文件的 API 为例
|
||||
var_dump($result["data"]);
|
||||
// 输出群文件列表
|
||||
```
|
||||
|
||||
3
docs/event/custom-annotations.md
Normal file
3
docs/event/custom-annotations.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 自定义注解
|
||||
|
||||
TODO:师傅,莫催,快肝完了!
|
||||
119
docs/event/event-dispatcher.md
Normal file
119
docs/event/event-dispatcher.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 事件分发器(进阶)
|
||||
|
||||
事件分发器是以上所有注解事件执行函数的一个分发器,如果你在上一章已经学会了如何创建自定义注解,那么本章就来说明如何用内置的事件分发器进行分发自定义事件。
|
||||
|
||||
如果你不需要了解或自定义有关事件分发的功能,此处可无需阅读。
|
||||
|
||||
## 属性
|
||||
|
||||
- 类名:`ZM\Event\EventDispatcher`
|
||||
|
||||
## 方法
|
||||
|
||||
### EventDispatcher::interrupt()
|
||||
|
||||
阻断当前正在运行的事件,只能在事件内部被调用的函数中实现。
|
||||
|
||||
### __construct()
|
||||
|
||||
构造方法。
|
||||
|
||||
```php
|
||||
EventDispatcher::__construct(string $class = '')
|
||||
```
|
||||
|
||||
初始化一个事件分发器,可进行一系列设置,对事件分发做限定。
|
||||
|
||||
#### 参数
|
||||
|
||||
`$class`:设置要分发的事件对应的注解类名,支持自定义注解(例如 `CQMessage::class`)
|
||||
|
||||
### setRuleFunction()
|
||||
|
||||
设置函数触发规则判定的函数(就是在执行事件函数前执行的规则判定)
|
||||
|
||||
```php
|
||||
setRuleFunction(callable $rule = null)
|
||||
```
|
||||
|
||||
#### 参数
|
||||
|
||||
`$rule`:支持回调或闭包。闭包的参数为执行对应事件函数所绑定的注解事件对象。
|
||||
|
||||
```php
|
||||
$dispatcher = new EventDispatcher(CustomEvent::class);
|
||||
$dispatcher->setRuleFunction(function($obj) {
|
||||
return $obj->name == "zhamao" ? true : false;
|
||||
});
|
||||
```
|
||||
|
||||
上方的 `$obj` 就是 CustomEvent 类的实例,参数绑定为注解中对应的参数。
|
||||
|
||||
### setResultFunction()
|
||||
|
||||
设置事件函数返回值处理的回调函数。
|
||||
|
||||
```php
|
||||
setReturnFunction(callable $return_func)
|
||||
```
|
||||
|
||||
#### 参数
|
||||
|
||||
`$return_func`:设置事件函数返回值处理的回调函数,回调参数绑定为对应单独事件函数的返回值。
|
||||
|
||||
```php
|
||||
$dispatcher = new EventDispatcher(CustomEvent::class);
|
||||
$dispatcher->setReturnFunction(function($return) {
|
||||
if (is_string($return)) Console::info("函数返回了 ".$return);
|
||||
});
|
||||
```
|
||||
|
||||
### dispatchEvents()
|
||||
|
||||
开始分发事件。
|
||||
|
||||
```php
|
||||
dispatchEvents(...$params)
|
||||
```
|
||||
|
||||
#### 参数
|
||||
|
||||
自定义参数,这里填入的参数将被填入被分发的函数参数中。
|
||||
|
||||
```php
|
||||
$dispatcher->dispatchEvents("foo", "bar");
|
||||
```
|
||||
|
||||
```php
|
||||
<?php
|
||||
class Test {
|
||||
/**
|
||||
* @CustomEvent("zhamao")
|
||||
*/
|
||||
public function test($arg1, $arg2) {
|
||||
echo "$arg1: $arg2"; //将输出 "foo: bar"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 机制
|
||||
|
||||
事件分发器的机制说简单不简单,说复杂也不复杂,它和中间件有着非常大的关系,因为它会自动检测和识别所要执行的函数有没有中间件,并且根据顺序进行执行。
|
||||
|
||||
在炸毛框架内部,一个完整的事件流程和中间件的关系如下图:
|
||||
|
||||

|
||||
|
||||
对于同一事件的优先级和响应顺序,优先级的关系如下图:
|
||||
|
||||

|
||||
|
||||
对于事件内单个事件被调用的单个函数下如果存在多个中间件,中间件模型和事件的关系如下图:
|
||||
|
||||

|
||||
|
||||
## 实战例子
|
||||
|
||||
我们假设 CustomEvent 是我们的自定义注解。还没写完,这部分太复杂了,而且举例子也不好举例,这块应该也不用着急更新。
|
||||
|
||||
TODO:待完成
|
||||
72
docs/event/framework-annotations.md
Normal file
72
docs/event/framework-annotations.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 框架核心注解事件
|
||||
|
||||
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
|
||||
|
||||
## OnSwooleEvent()
|
||||
|
||||
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接,HTTP 请求到达等。这个是旧的统一的 Swoole 事件分发注解。请尽量使用上面几个新的注解。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ------------ | ------------------------------------------ |
|
||||
| 名称 | `@OnSwooleEvent` |
|
||||
| 触发前提 | 当参数指定的 `type` 对应的事件被触发后激活 |
|
||||
| 命名空间 | `ZM\Annotation\Swoole\OnSwooleEvent` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 无 |
|
||||
| 注解绑定参数 | |
|
||||
|
||||
### 注解参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| -------- | -------------------------------------------------------- | ----------------------------------------------- | ---------------- |
|
||||
| type | `string`,支持填入 `open`,`request`,`close`,`message` | 限定事件的类型,**必填** | |
|
||||
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 例如判断连接是否为 QQ 机器人(`connectIsQQ()`) | 空,rule 为 true |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 事件绑定参数
|
||||
|
||||
`$conn`: [ConnectionObject](/advanced/inside-class/) 类型,返回一个当前 WS 连接的连接对象。
|
||||
|
||||
### 示例1(机器人连接框架后输出信息)
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
class Hello {
|
||||
/**
|
||||
* 在机器人客户端连接框架后向终端输出信息
|
||||
* @OnSwooleEvent("open",rule="connectIsQQ()")
|
||||
* @param $conn
|
||||
*/
|
||||
public function onConnect(ConnectionObject $conn) {
|
||||
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里的 Console 是终端输出组件,详情见组件一栏对应的文档查询。
|
||||
|
||||
### 示例2(阻断 Chrome 访问框架时多访问一次的问题)
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\Event\EventDispatcher;
|
||||
class Hello {
|
||||
/**
|
||||
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
|
||||
* @OnSwooleEvent("request",rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
|
||||
*/
|
||||
public function onRequest() {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中 EventDispatcher 为事件分发器,interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
|
||||
70
docs/event/index.md
Normal file
70
docs/event/index.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 事件和注解
|
||||
|
||||
## 注解事件概念
|
||||
|
||||
我们知道事件,是一个底层的 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` 等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
|
||||
|
||||
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
|
||||
|
||||
## 阻断事件
|
||||
|
||||
由于炸毛框架内的注解事件统一由一个通用的事件分发器进行分发,所以你在任何注解事件内都可以用通用的方式阻断当前正在运行的事件。
|
||||
|
||||
首先就是要记得先 use 事件分发器的类:`use ZM\Event\EventDispatcher;`。
|
||||
|
||||
```php
|
||||
EventDispatcher::interrupt();
|
||||
EventDispatcher::interrupt($data); // 也可以带返回值,自定义注解事件时有用。
|
||||
```
|
||||
|
||||
|
||||
|
||||
3
docs/event/middleware.md
Normal file
3
docs/event/middleware.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 中间件注解
|
||||
|
||||
TODO:师傅,莫催,快肝完了!
|
||||
328
docs/event/robot-annotations.md
Normal file
328
docs/event/robot-annotations.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 机器人注解事件
|
||||
|
||||
QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理后触发到单个类中方法的事件。
|
||||
|
||||
为了便于开发,这里的注解类对应 CQHTTP 插件返回的 `post_type` 类型,对号入座即可。
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
在使用注解绑定事件过程中,如果无 **必需** 参数,可一个参数也不写,效果就是此事件任何情况下都会调用此方法。例如:`@CQMessage()`
|
||||
|
||||
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
|
||||
|
||||
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand),`@CQMessage` 注解处理。
|
||||
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
|
||||
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
|
||||
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
|
||||
|
||||
## CQMessage()
|
||||
|
||||
QQ 收到消息后触发的事件对应注解。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---- | ----------- |
|
||||
| 名称 | `@CQMessage` |
|
||||
| 触发前提 | 当 `post_type` 为 `message` 时触发 |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQMessage` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| ------------ | ------------------------------------- | -------------------------------------- | ---- |
|
||||
| message_type | `string`,支持填入 `private`,`group` | 限定消息事件的来源类型,如私聊或群消息 | 空 |
|
||||
| user_id | `int64` 或 `string` | 限定消息发送用户 ID(QQ 号) | 空 |
|
||||
| group_id | `int64` 或 `string` | 限定消息发送来源群 ID(QQ 群号) | 空 |
|
||||
| message | `string` | 限定消息内容文本 | 空 |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 用法
|
||||
|
||||
下面这个例子的注释用途就是:
|
||||
|
||||
- 在用户 QQ 为 `123456` 的用户私聊给机器人发消息后机器人回复内容。
|
||||
- 用户发送文字为 `hello` 时返回 `你好啊,xxx` 的消息。
|
||||
|
||||
=== "代码"
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQMessage(message_type="private",user_id=123456)
|
||||
*/
|
||||
public function test() {
|
||||
return "你和机器人私聊发送了这些文本:".ctx()->getMessage();
|
||||
}
|
||||
/**
|
||||
* @CQMessage(message="hello")
|
||||
*/
|
||||
public function hello() {
|
||||
return "你好啊,".ctx()->getUserId();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
) 假设我是私聊机器人
|
||||
( 你和机器人私聊发送了这些文本:假设我是私聊机器人
|
||||
^ 假设我现在切到群里,在群里发hello
|
||||
) hello
|
||||
( 你好啊,123456
|
||||
</chat-box>
|
||||
|
||||
## CQCommand()
|
||||
|
||||
此注解是对 `@CQMessage` 类别的再封装,是命令解析格式处理消息的利器。例如,你想写一个疫情上报,指令是 `疫情 城市名称`,那么此方式来解析用户消息会更加方便。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | -------------------------------------------------------- |
|
||||
| 名称 | `@CQCommand` |
|
||||
| 触发前提 | 当根据参数规则匹配到用户命令式消息时触发 |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQCommand` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| ------------ | ------------------------------------- | ------------------------------------------------------ | ---- |
|
||||
| match | `string` | 匹配第一个词的命令式消息,如 `天气 北京` 中的 `天气` | 空 |
|
||||
| pattern | `string` | 根据 * 号通配符进行模式匹配用户消息,如 `查询*天气` | 空 |
|
||||
| regex | `string`,限定正则表达式 | 匹配正则表达式匹配到的用户消息 | 空 |
|
||||
| start_with | `string` | 匹配消息开头相匹配的消息,如 `我叫炸毛`,这里写 `我叫` | 空 |
|
||||
| end_with | `string` | 匹配消息结尾相匹配的消息,以 `start_with` 类推 | 空 |
|
||||
| keyword | `string` | 匹配消息中有相关关键词的消息 | 空 |
|
||||
| alias | `array[string]` | `match` 匹配到命令的别名,数组形式 | `{}` |
|
||||
| message_type | `string`,支持填入 `private`,`group` | 限定消息事件的来源类型,同 `@CQMessage` | 空 |
|
||||
| user_id | `int64` 或 `string` | 限定消息发送用户 ID,同 `@CQMessage` | 空 |
|
||||
| group_id | `int64` 或 `string` | 限定消息发送来源群 ID,同 `@CQMessage` | 空 |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
!!! warning "注意"
|
||||
|
||||
在 `@CQCommand` 注解事件中,从 `match` 到 `keyword` 六个参数中,必须且只能定义一个,`alias` 目前只能和 `match` 参数同时使用;
|
||||
|
||||
框架内部对于同一条消息事件,优先处理 `@CQCommand` 注解事件,如果未匹配到任何注解事件,则才会继续执行 `@CQMessage` 注解事件。
|
||||
|
||||
- 参数 `match` 匹配模式是:遇到空格、换行就会切分,比如 `点歌 xxx yyy` 会被分割为 `[点歌,xxx,yyy]`,然后抽取第一个词做为命令去匹配,剩下的为参数。
|
||||
- 参数 `pattern` 匹配模式是:\* 号位置变成参数,比如 `从*到*的随机数`,我们输入 `从1到9的随机数`,成功匹配,参数列表:`[1,9]`。
|
||||
- 参数 `regex` 匹配模式为 PHP 标准的 pcre 正则表达式,比如 `([01][0-9][2][0-3]):[0-5][0-9]` 用来匹配 `22:45`。
|
||||
- 参数 `start_with`, `end_with` 和 `keyword` 都是根据消息内容开头、结尾或者内容包含是否匹配来匹配,这里就不多说了,你懂的。
|
||||
- 参数 `alias` 用的时候一般是这样:`@CQCommand(match="你好",alias={"你好啊","你是谁"})`,用以扩充同义词下命令的适配广度。
|
||||
|
||||
### 用法
|
||||
|
||||
我们以参数 `match` 写一个简单的 demo:
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQCommand(match="疫情",alias={"COVID"})
|
||||
*/
|
||||
public function virus(){
|
||||
$city = ctx()->getNextArg("请输入城市名称");
|
||||
return "城市 ".$city." 的疫情状况如下:"."{这里假装是疫情接口返回的数据}";
|
||||
}
|
||||
/**
|
||||
* 如果选择使用 match 参数的话,可以省略 `match=`
|
||||
* @CQCommand("掷硬币")
|
||||
*/
|
||||
public function randChoice() {
|
||||
return "你看到的是:" . (mt_rand(0,1) ? "正面" : "反面");
|
||||
}
|
||||
/**
|
||||
* @CQCommand(pattern="*把*翻译成*")
|
||||
*/
|
||||
public function translate() {
|
||||
ctx()->getNextArg(); // 为什么需要单独调用一次呢?看下面例子就知道啦
|
||||
$text = ctx()->getNextArg(); // 获取第二个星号匹配的内容
|
||||
$target = ctx()->getNextArg(); // 获取第三个星号匹配的内容
|
||||
// 这里 FakeTranslateAPI 是假设我们对接了一个翻译的 API,开发时请替换为自己的接口。
|
||||
return "翻译结果:" . FakeTranslateAPI::translate($text, $target);
|
||||
}
|
||||
}
|
||||
```
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
) 疫情 北京
|
||||
( 城市 北京 的疫情状况如下:blablablabla
|
||||
) COVID 香港
|
||||
( 城市 香港 的疫情状况如下:blablablabla
|
||||
) 掷硬币
|
||||
( 你看到的是:正面
|
||||
) 我想把我爱你翻译成英语
|
||||
( 翻译结果:I love you!
|
||||
</chat-box>
|
||||
|
||||
## CQNotice()
|
||||
|
||||
通知事件。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ----------------------------------------------------- |
|
||||
| 名称 | `@CQNotice` |
|
||||
| 触发前提 | 当 `post_type` 为 `notice` 时触发(通知类事件上报时) |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQNotice` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 无作用 |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| ----------- | ------------------------------------ | ------------------------------------------------------------ | ---- |
|
||||
| notice_type | `string`,支持填入 onebot 标准的内容 | 限定通知事件的类型,见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
|
||||
| user_id | `int64` 或 `string` | 限定通知事件用户 ID(QQ 号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
|
||||
| group_id | `int64` 或 `string` | 限定通知事件群 ID(QQ 群号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
|
||||
| operator_id | `int64` 或 `string` | 限定操作者 QQ 号,同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 用法
|
||||
|
||||
TODO:先放着,有时间再更。
|
||||
|
||||
## CQRequest()
|
||||
|
||||
请求事件。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ------------------------------------------------------ |
|
||||
| 名称 | `@CQRequest` |
|
||||
| 触发前提 | 当 `post_type` 为 `request` 时触发(通知类事件上报时) |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQRequest` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 无作用 |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| ------------ | ------------------------------------ | ------------------------------------------------------------ | ---- |
|
||||
| request_type | `string`,支持填入 onebot 标准的内容 | 限定请求事件的类型,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
|
||||
| user_id | `int64` 或 `string` | 限定请求事件当事人用户 ID(QQ 号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
|
||||
| sub_type | `string` | 限定请求事件来源群 ID(QQ 群号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
|
||||
| comment | `string` | 限定验证消息内容,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 用法
|
||||
|
||||
TODO:先放着,有时间再更。
|
||||
|
||||
## CQMetaEvent()
|
||||
|
||||
元事件,元事件不属于用户交互的一部分,消息、通知、请求三大类事件是与聊天软件直接相关的、机器人真实接收到的事件,除了这些,OneBot 自己还会产生一类事件,这里称之为「元事件」,例如生命周期事件、心跳事件等,这类事件与兼容 OneBot 的客户端和炸毛框架本身的运行状态有关,而与聊天软件无关。元事件的上报方式和普通事件完全一样。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ----------------------------------------------------- |
|
||||
| 名称 | `@CQMetaEvent` |
|
||||
| 触发前提 | 当 `post_type` 为 `meta_event` 时触发(元事件上报时) |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQMetaEvent` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 无作用 |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| --------------- | ------------------ | ------------------------------------------------------------ | ---- |
|
||||
| meta_event_type | `string`,**必需** | 限定元事件的类型,见 [OneBot - 元事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/meta.md) | |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 用法
|
||||
|
||||
TODO:先放着,有时间再更。
|
||||
|
||||
## CQBefore()
|
||||
|
||||
所有机器人事件的前置注解事件,一般用作消息过滤、全局日志、全局替换等。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ------------------------------------------------------------ |
|
||||
| 名称 | `@CQBefore` |
|
||||
| 触发前提 | 当 `post_type` 等于参数 `cq_event` 时触发 |
|
||||
| 命名空间 | `ZM\Annotation\CQ\CQBefore` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 仅可返回 `bool`,如果为 `false`,则阻断 `cq_event` 类的所有事件防止被执行 |
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| -------- | ------------------------------------------------------------ | ------------------------ | ---- |
|
||||
| cq_event | `string`,**必需**,支持 `message`,`notice`,`request`,`meta_event` | 限定机器人时间的类型 | |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
### 用法
|
||||
|
||||
=== "代码"
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQBefore;
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
class Test {
|
||||
/**
|
||||
* @CQBefore("message")
|
||||
*/
|
||||
public function filter(){
|
||||
// 可用于敏感词,如政治相关的词语不响应其他模块
|
||||
if(mb_strpos(ctx()->getMessage(), "谷歌") !== false) return false;
|
||||
else return true;
|
||||
}
|
||||
/**
|
||||
* @CQCommand("百科")
|
||||
*/
|
||||
public function wiki() {
|
||||
$content = ctx()->getNextArg("请说你要查百科的内容");
|
||||
// 这里假设你对接了一个查百科的接口
|
||||
return "已搜到匹配 $content 的如下结果:".FakeAPI::searchWiki($content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
) 百科 北京
|
||||
( 已搜到匹配 北京 的如下结果:blablabla
|
||||
) 百科 谷歌被封
|
||||
^ 机器人没有任何回复
|
||||
|
||||
!!! warning "注意"
|
||||
|
||||
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
|
||||
|
||||
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
|
||||
|
||||
## CQAfter()
|
||||
|
||||
同上。只是在以上所有事件都调用后才会调用的。
|
||||
|
||||
## CQAPIResponse()
|
||||
|
||||
TODO:还没写完,先放着,有时间再更。
|
||||
231
docs/event/route-annotations.md
Normal file
231
docs/event/route-annotations.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 路由注解事件
|
||||
|
||||
炸毛框架提供了一个简易但是高效易用的 HTTP 路由注解,你可以使用路由功能来开发任何 Web 应用微服务、API 接口、中间件等。
|
||||
|
||||
!!! quote "开发提示"
|
||||
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不罗嗦。
|
||||
|
||||
## 控制器和路由
|
||||
|
||||
Controller 和 Route 为路由注解事件的核心注解事件,其中 Controller 的注解事件为 `@Controller`,Route 的注解事件为 `@RequestMapping`。
|
||||
|
||||
### Controller()
|
||||
|
||||
#### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ------------------------------- |
|
||||
| 名称 | `@Controller` |
|
||||
| 触发前提 | 当路由 url 匹配到时进入触发 |
|
||||
| 命名空间 | `ZM\Annotation\Http\Controller` |
|
||||
| 适用位置 | 类 |
|
||||
| 返回值处理 | 对类注解修饰,无返回值 |
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| -------- | -------------- | ------------ | ---- |
|
||||
| prefix | `string`,必需 | 控制器的 url | 空 |
|
||||
|
||||
### RequestMapping()
|
||||
|
||||
#### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ----------------------------------------------------------- |
|
||||
| 名称 | `@RequestMapping` |
|
||||
| 触发前提 | 当路由 url 匹配到时进入触发 |
|
||||
| 命名空间 | `ZM\Annotation\Http\RequestMapping` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 返回类型是 `string` 时,自动调用 HTTP 响应并返回 200 状态码 |
|
||||
|
||||
#### 参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| -------------- | ----------------------------------------------------------- | -------------------------- | ------------------------------------------ |
|
||||
| route | `string`,必需 | 控制器的 url | 空 |
|
||||
| name | `string` | 路由的名称 | 空 |
|
||||
| request_method | `array`,限定 `RequestMethod::GET` 等常量 | 限制激活路由的 HTTP 方法 | `[RequestMethod::GET,RequestMethod::POST]` |
|
||||
| params | `array`,当路由中含有如 `{id}` 类似的动态路由时,会动态改变 | 动态参数的路由参数值的绑定 | `[]` |
|
||||
|
||||
#### 函数调用参数
|
||||
|
||||
- `$param`:如果路由中存在变量(动态路由),则会把动态路由所匹配的参数放入 `$param` 数组中。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping(route="/test/{ass}")
|
||||
*/
|
||||
public function testName($param) {
|
||||
return "Your name is ".($param["ass"] ?? "unknown");
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 路由示例
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Controller;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
/**
|
||||
* @Controller("/api")
|
||||
*/
|
||||
class Hello {
|
||||
/**
|
||||
* @RequestMapping("/index")
|
||||
*/
|
||||
public function index(){
|
||||
ctx()->getResponse()->end("This is API index page"); // 使用上下文获取响应对象
|
||||
}
|
||||
/**
|
||||
* @RequestMapping("/ping")
|
||||
*/
|
||||
public function ping(){
|
||||
return "pong"; // 直接返回字符串
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
!!! example "效果描述"
|
||||
当访问浏览器的 `http://localhost:20001/api/index` 时,浏览器会返回 `This is API index page`,当访问 `/api/ping` 的 url 时,浏览器会返回 `pong`。
|
||||
|
||||
```
|
||||
/ -> 无任何路由
|
||||
/api/index -> Hello->index
|
||||
/api/ping -> Hello->ping
|
||||
```
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
当 `@Controller` 为 `/` 的时候,效果和不写是一样的,`@RequestMapping` 为 `/` 或 `/index/inside` 等多级路由也是可以的。
|
||||
|
||||
### 绑定参数
|
||||
|
||||
在 `@RequestMapping` 中,不仅可以写静态的路由地址,也可以写绑定的参数。例如:`@RequestMapping(route="/index/{name}")`,则访问 `/index/xxx` 的时候,你在函数方法内可以这样获取此参数:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/index/{name}")
|
||||
*/
|
||||
public function index($arg) {
|
||||
return "Your param 'name' is ".$arg["name"];
|
||||
}
|
||||
```
|
||||
|
||||
## 获取请求参数 GET / POST
|
||||
|
||||
炸毛框架支持获取外部 HTTP 请求进来的 GET 和 POST 请求,通过获取 HTTP 请求对象 [Request](/advanced/inside-class/) 即可。对象具体属性和方法点这个链接进去就行。
|
||||
|
||||
### 示例
|
||||
|
||||
=== "获取 GET"
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$get = ctx()->getRequest()->get;
|
||||
if(isset($get["name"])) return "hello, ".$get["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
=== "获取 POST(x-www-form-urlencoded)"
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->post;
|
||||
if(isset($post["name"])) return "hello, ".$post["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
=== "获取 JSON POST"
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->rawContent();
|
||||
$json = json_decode($post, true);
|
||||
if ($json === null) return "Invalid json data!";
|
||||
if(isset($json["name"])) return "hello, ".$json["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
## 设置路由请求方式
|
||||
|
||||
如果想要设置允许请求控制器的 HTTP 请求方式,可以使用方法在控制器中的 `@RequestMapping` 注解配置 `method` 参数,可以是 `GET`,`POST`,`PUT`, `PATCH`,`DELETE`,`OPTIONS`,`HEAD` 中的一个或多个。
|
||||
|
||||
- 限定 HTTP 方法:`@RequestMapping(method="GET")`,`@RequestMapping(method={"GET","POST"})`
|
||||
|
||||
## 静态文件服务器
|
||||
|
||||
框架支持了静态文件的访问。如需使用,则需要先到配置文件中配置相应的 `static_file_server` 参数中 `status` 为 `true`。
|
||||
|
||||
框架分为两种静态文件服务器,一种是全局的静态文件服务器,比如框架部署在 `http://127.0.0.1:20001/` 上通过 HTTP 访问,如果没有访问到 `@RequestMapping` 注解事件注册的路由地址,则会通过 url 自动查找静态文件服务器设置的根路径下面的文件,如果都不存在则会返回 404。
|
||||
|
||||
### 配置全局静态文件服务器
|
||||
|
||||
我们假设在你写的框架应用的根目录下,有如下文件和内容:
|
||||
|
||||
```
|
||||
resources/html/hello.html (下面是内容)
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
框架文档内容太多了,写不完!!!
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
然后在 `global.php` 配置文件中静态文件服务器参数为:
|
||||
|
||||
```php
|
||||
/** 静态文件访问 */
|
||||
$config['static_file_server'] = [
|
||||
'status' => true,
|
||||
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
|
||||
'document_index' => [
|
||||
'index.html'
|
||||
]
|
||||
];
|
||||
```
|
||||
|
||||
最终,我们通过 `vendor/bin/start server` 等方式,启动框架后,浏览器访问 `http://127.0.0.1:20001/hello.html` 即可获取内容。
|
||||
|
||||
### 配置局部静态文件服务器
|
||||
|
||||
所涉及的类的命名空间:`use ZM\Http\StaticFileHandler;`
|
||||
|
||||
局部静态文件服务器一般用于,比如机器人要发送图片,或者给其他 HTTP 服务提供文件下载的接口时可用。我们假设写了一个图片收集的一个静态文件夹区域,将其中一个子路由当作图片静态目录:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/images/{filename}")
|
||||
* @param $param
|
||||
* @return StaticFileHandler
|
||||
*/
|
||||
public function staticImage($param) {
|
||||
Console::info("[下载图片] " . $param["filename"]);
|
||||
return new StaticFileHandler($param["filename"], "/path/to/your/image_dir/");
|
||||
}
|
||||
```
|
||||
|
||||
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
|
||||
142
docs/guide/basic-config.md
Normal file
142
docs/guide/basic-config.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/installation.md
Normal file
134
docs/guide/installation.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/) 部分!
|
||||
23
docs/guide/onebot-choose.md
Normal file
23
docs/guide/onebot-choose.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 协议聊天模拟器。但目前还处在开发中,暂不可用。
|
||||
4
docs/guide/quickstart-http.md
Normal file
4
docs/guide/quickstart-http.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# 快速上手 - HTTP 服务器篇
|
||||
|
||||
HTTP 服务器篇暂时先放一放,大家应该主要都是奔着机器人开发来的吧~
|
||||
|
||||
228
docs/guide/quickstart-robot.md
Normal file
228
docs/guide/quickstart-robot.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/register-event.md
Normal file
66
docs/guide/register-event.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/write-module.md
Normal file
68
docs/guide/write-module.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>
|
||||
92
docs/javascripts/config.js
Normal file
92
docs/javascripts/config.js
Normal file
@@ -0,0 +1,92 @@
|
||||
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>';
|
||||
} else if (j.substr(0, 2) === '^ ') {
|
||||
final += '<div class="doc-chat-row doc-chat-banner">' + j.substr(2) + '</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"><img src="' + j.substr(2) + '" alt=""/></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
|
||||
|
||||
正式版发布。
|
||||
26
docs/update/v2.md
Normal file
26
docs/update/v2.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# 更新日志(v2 版本)
|
||||
|
||||
## v2.0.3
|
||||
|
||||
> 更新事件:2020.12.31
|
||||
|
||||
- 修复:CQBefore 注解事件在 level 低于 200 时无法调用的 bug
|
||||
- 修复:CQMetaEvent 注解事件调用时报错的 bug
|
||||
|
||||
## v2.0.2
|
||||
|
||||
> 更新时间:2020.12.31
|
||||
|
||||
- 更新:将 CQ 码调用类更新到与最新 OneBot 标准相兼容的状态
|
||||
|
||||
## v2.0.1
|
||||
|
||||
> 更新时间:2020.12.23
|
||||
|
||||
- 修复:开屏报错文件夹不存在
|
||||
|
||||
## v2.0
|
||||
|
||||
> 更新时间:2020.12.23
|
||||
|
||||
已发布正式版。
|
||||
83
mkdocs.yml
Normal file
83
mkdocs.yml
Normal file
@@ -0,0 +1,83 @@
|
||||
site_name: 炸毛框架 v2
|
||||
|
||||
repo_name: '炸毛框架'
|
||||
repo_url: 'https://github.com/zhamao-robot/zhamao-framework'
|
||||
edit_uri: 'blob/master/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/installation.md
|
||||
- 快速上手(机器人篇): guide/quickstart-robot.md
|
||||
- 快速上手(HTTP篇): guide/quickstart-http.md
|
||||
- 选择聊天机器人实例: guide/onebot-choose.md
|
||||
- 基本配置: guide/basic-config.md
|
||||
- 编写模块: guide/write-module.md
|
||||
- 注册事件响应: guide/register-event.md
|
||||
- 事件和注解:
|
||||
- 事件和注解: event/index.md
|
||||
- 机器人注解事件: event/robot-annotations.md
|
||||
- HTTP 路由注解事件: event/route-annotations.md
|
||||
- 框架核心注解事件: event/framework-annotations.md
|
||||
- 中间件注解: event/middleware.md
|
||||
- 自定义注解: event/custom-annotations.md
|
||||
- 事件分发器: event/event-dispatcher.md
|
||||
- 框架组件:
|
||||
- 框架组件: component/index.md
|
||||
- 机器人 API: component/robot-api.md
|
||||
- CQ 码(多媒体消息): component/cqcode.md
|
||||
- 上下文: component/context.md
|
||||
- 进阶开发:
|
||||
- 进阶开发: advanced/index.md
|
||||
- 从 v1 升级: advanced/to-v2.md
|
||||
- 内部类文件手册: advanced/inside-class.md
|
||||
- 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php #plain
|
||||
|
||||
//这里写你的全局函数
|
||||
function pgo(callable $func, $name = "default") {
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\Annotation\Swoole\OnCloseEvent;
|
||||
use ZM\Annotation\Swoole\OnOpenEvent;
|
||||
use ZM\Annotation\Swoole\OnRequestEvent;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Store\Redis\ZMRedis;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
/**
|
||||
* Class Hello
|
||||
@@ -18,33 +21,19 @@ use ZM\Store\Redis\ZMRedis;
|
||||
class Hello
|
||||
{
|
||||
/**
|
||||
* 一个简单的redis连接池使用demo,将下方user_id改为你自己的QQ号即可(为了不被不法分子利用)
|
||||
* @CQCommand("redis_test",user_id=627577391)
|
||||
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
|
||||
* @CQCommand(".reload",user_id=627577391)
|
||||
*/
|
||||
public function testCase() {
|
||||
$a = new ZMRedis();
|
||||
$redis = $a->get();
|
||||
$r1 = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想设置的操作[r/w]");
|
||||
switch ($r1) {
|
||||
case "r":
|
||||
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想读取的键名");
|
||||
$result = $redis->get($k);
|
||||
ctx()->reply("结果:" . $result);
|
||||
break;
|
||||
case "w":
|
||||
$k = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的键名");
|
||||
$v = ctx()->getArgs(ZM_MATCH_FIRST, "请说出你想写入的字符串");
|
||||
$result = $redis->set($k, $v);
|
||||
ctx()->reply("结果:" . ($result ? "成功" : "失败"));
|
||||
break;
|
||||
}
|
||||
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"];
|
||||
}
|
||||
|
||||
@@ -105,7 +94,7 @@ class Hello
|
||||
|
||||
/**
|
||||
* 在机器人连接后向终端输出信息
|
||||
* @OnSwooleEvent("open",rule="connectIsQQ()")
|
||||
* @OnOpenEvent("qq")
|
||||
* @param $conn
|
||||
*/
|
||||
public function onConnect(ConnectionObject $conn) {
|
||||
@@ -114,16 +103,24 @@ class Hello
|
||||
|
||||
/**
|
||||
* 在机器人断开连接后向终端输出信息
|
||||
* @OnSwooleEvent("close",rule="connectIsQQ()")
|
||||
* @OnCloseEvent("qq")
|
||||
* @param ConnectionObject $conn
|
||||
*/
|
||||
public function onDisconnect(ConnectionObject $conn) {
|
||||
Console::info("机器人 " . $conn->getOption("connect_id") . " 已断开连接!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
|
||||
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
|
||||
*/
|
||||
public function onRequest() {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* 框架会默认关闭未知的WebSocket链接,因为这个绑定的事件,你可以根据你自己的需求进行修改
|
||||
* @OnSwooleEvent(type="open",rule="connectIsDefault()")
|
||||
* @OnOpenEvent("default")
|
||||
*/
|
||||
public function closeUnknownConn() {
|
||||
Console::info("Unknown connection , I will close it.");
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace Module\Middleware;
|
||||
|
||||
use ZM\Annotation\Http\After;
|
||||
use ZM\Annotation\Http\Before;
|
||||
use Exception;
|
||||
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 +21,7 @@ class TimerMiddleware implements MiddlewareInterface
|
||||
private $starttime;
|
||||
|
||||
/**
|
||||
* @Before()
|
||||
* @HandleBefore()
|
||||
* @return bool
|
||||
*/
|
||||
public function onBefore() {
|
||||
@@ -28,9 +30,19 @@ class TimerMiddleware implements MiddlewareInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @After()
|
||||
* @HandleAfter()
|
||||
*/
|
||||
public function onAfter() {
|
||||
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleException(\Exception::class)
|
||||
* @param Exception $e
|
||||
* @throws Exception
|
||||
*/
|
||||
public function onException(Exception $e) {
|
||||
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,70 +34,59 @@ class CQ
|
||||
return " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送emoji表情
|
||||
* @param $id
|
||||
* @return string
|
||||
*/
|
||||
public static function emoji($id) {
|
||||
if (is_numeric($id)) {
|
||||
return "[CQ:emoji,id=" . $id . "]";
|
||||
}
|
||||
Console::warning("传入的emoji id($id)错误!");
|
||||
return " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送原创表情,存放在酷Q目录的data/bface/下
|
||||
* @param $id
|
||||
* @return string
|
||||
*/
|
||||
public static function bface($id) {
|
||||
return "[CQ:bface,id=" . $id . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送小表情
|
||||
* @param $id
|
||||
* @return string
|
||||
*/
|
||||
public static function sface($id) {
|
||||
if (is_numeric($id)) {
|
||||
return "[CQ:sface,id=" . $id . "]";
|
||||
}
|
||||
Console::warning("传入的sface id($id)错误!");
|
||||
return " ";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送图片
|
||||
* cache为<FALSE>时禁用CQ-HTTP-API插件的缓存
|
||||
* @param $file
|
||||
* @param bool $cache
|
||||
* @param bool $flash
|
||||
* @param bool $proxy
|
||||
* @param int $timeout
|
||||
* @return string
|
||||
*/
|
||||
public static function image($file, $cache = true) {
|
||||
if ($cache === false)
|
||||
return "[CQ:image,file=" . $file . ",cache=0]";
|
||||
else
|
||||
return "[CQ:image,file=" . $file . "]";
|
||||
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:image,file=" . $file .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($flash ? ",type=flash" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送语音
|
||||
* cache为<FALSE>时禁用CQ-HTTP-API插件的缓存
|
||||
* magic为<TRUE>时标记为变声
|
||||
* @param $file
|
||||
* @param bool $magic
|
||||
* @param bool $cache
|
||||
* @param bool $proxy
|
||||
* @param int $timeout
|
||||
* @return string
|
||||
*/
|
||||
public static function record($file, $magic = false, $cache = true) {
|
||||
if ($cache === false) $c = ",cache=0";
|
||||
else $c = "";
|
||||
if ($magic === true) $m = ",magic=true";
|
||||
else $m = "";
|
||||
return "[CQ:record,file=" . $file . $c . $m . "]";
|
||||
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:record,file=" . $file .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($magic ? ",magic=1" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短视频
|
||||
* @param $file
|
||||
* @param bool $cache
|
||||
* @param bool $proxy
|
||||
* @param int $timeout
|
||||
* @return string
|
||||
*/
|
||||
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:video,file=" . $file .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,6 +113,56 @@ class CQ
|
||||
return "[CQ:shake]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送新的戳一戳
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
public static function poke($type, $id, $name = "") {
|
||||
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送匿名消息
|
||||
* @param int $ignore
|
||||
* @return string
|
||||
*/
|
||||
public static function anonymous($ignore = 1) {
|
||||
return "[CQ:anonymous".($ignore != 1 ? ",ignore=0" : "")."]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送链接分享(只能在单条回复中单独使用)
|
||||
* @param $url
|
||||
* @param $title
|
||||
* @param null $content
|
||||
* @param null $image
|
||||
* @return string
|
||||
*/
|
||||
public static function share($url, $title, $content = null, $image = null) {
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送好友或群推荐名片
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @return string
|
||||
*/
|
||||
public static function contact($type, $id) {
|
||||
return "[CQ:contact,type=$type,id=$id]";
|
||||
}
|
||||
|
||||
public static function location($lat, $lon, $title = "", $content = "") {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送音乐分享(只能在单条回复中单独使用)
|
||||
* qq、163、xiami为内置分享,需要先通过搜索功能获取id后使用
|
||||
@@ -136,10 +175,10 @@ class CQ
|
||||
* $image 为音乐卡片的图片链接地址(可忽略)
|
||||
* @param $type
|
||||
* @param $id_or_url
|
||||
* @param string $audio
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param string $image
|
||||
* @param null $audio
|
||||
* @param null $title
|
||||
* @param null $content
|
||||
* @param null $image
|
||||
* @return string
|
||||
*/
|
||||
public static function music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null) {
|
||||
@@ -164,20 +203,12 @@ class CQ
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送链接分享(只能在单条回复中单独使用)
|
||||
* @param $url
|
||||
* @param $title
|
||||
* @param null $content
|
||||
* @param null $image
|
||||
* @return string
|
||||
*/
|
||||
public static function share($url, $title, $content = null, $image = null) {
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
|
||||
public static function forward($id) {
|
||||
return "[CQ:forward,id=$id]";
|
||||
}
|
||||
|
||||
public static function node($user_id, $nickname, $content) {
|
||||
return "[CQ:node,user_id=$user_id,nickname=$nickname,content=".self::escape($content)."]";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -33,7 +33,7 @@ class AnnotationParser
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->start_time = microtime(true);
|
||||
$this->loadAnnotationClasses();
|
||||
//$this->loadAnnotationClasses();
|
||||
$this->req_mapping[0] = [
|
||||
'id' => 0,
|
||||
'pid' => -1,
|
||||
@@ -100,7 +100,7 @@ class AnnotationParser
|
||||
unset($this->annotation_map[$v]);
|
||||
continue 2;
|
||||
} elseif ($vs instanceof MiddlewareClass) {
|
||||
Console::verbose("正在注册中间件 " . $reflection_class->getName());
|
||||
Console::debug("正在注册中间件 " . $reflection_class->getName());
|
||||
$rs = $this->registerMiddleware($vs, $reflection_class);
|
||||
$this->middlewares[$rs["name"]] = $rs;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -297,7 +297,7 @@ class AnnotationParser
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function sortByLevel(&$events, string $class_name, $prefix = "") {
|
||||
public function sortByLevel(&$events, string $class_name, $prefix = "") {
|
||||
if (is_a($class_name, Level::class, true)) {
|
||||
$class_name .= $prefix;
|
||||
usort($events[$class_name], function ($a, $b) {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -21,8 +21,6 @@ class CQMetaEvent extends AnnotationBase implements Level
|
||||
* @Required()
|
||||
*/
|
||||
public $meta_event_type = '';
|
||||
/** @var string */
|
||||
public $sub_type = '';
|
||||
/** @var int */
|
||||
public $level;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
22
src/ZM/Annotation/Swoole/OnCloseEvent.php
Normal file
22
src/ZM/Annotation/Swoole/OnCloseEvent.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\Interfaces\Rule;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* Class OnCloseEvent
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnCloseEvent extends OnSwooleEventBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $connect_type = "default";
|
||||
}
|
||||
21
src/ZM/Annotation/Swoole/OnMessageEvent.php
Normal file
21
src/ZM/Annotation/Swoole/OnMessageEvent.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* Class OnMessageEvent
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnMessageEvent extends OnSwooleEventBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $connect_type = "default";
|
||||
}
|
||||
21
src/ZM/Annotation/Swoole/OnOpenEvent.php
Normal file
21
src/ZM/Annotation/Swoole/OnOpenEvent.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* Class OnOpenEvent
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnOpenEvent extends OnSwooleEventBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $connect_type = "default";
|
||||
}
|
||||
17
src/ZM/Annotation/Swoole/OnRequestEvent.php
Normal file
17
src/ZM/Annotation/Swoole/OnRequestEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace ZM\Annotation\Swoole;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("METHOD")
|
||||
* Class OnRequestEvent
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnRequestEvent extends OnSwooleEventBase
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -10,9 +10,9 @@ use ZM\Annotation\AnnotationBase;
|
||||
* Class OnWorkerStart
|
||||
* @package ZM\Annotation\Swoole
|
||||
* @Annotation
|
||||
* @Target("ALL")
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
class OnWorkerStart extends AnnotationBase
|
||||
class OnStart extends AnnotationBase
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
@@ -5,17 +5,14 @@ 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 OnSwooleEvent
|
||||
* @Annotation
|
||||
* @Target("ALL")
|
||||
* @Target("METHOD")
|
||||
* @package ZM\Annotation\Swoole
|
||||
*/
|
||||
class OnSwooleEvent extends AnnotationBase implements Rule, Level
|
||||
class OnSwooleEvent extends OnSwooleEventBase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
@@ -23,14 +20,6 @@ class OnSwooleEvent extends AnnotationBase implements Rule, Level
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/** @var string */
|
||||
public $rule = "";
|
||||
|
||||
/** @var int */
|
||||
public $level = 20;
|
||||
|
||||
public $callback = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
@@ -44,33 +33,4 @@ class OnSwooleEvent extends AnnotationBase implements Rule, Level
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,51 +4,26 @@
|
||||
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
|
||||
abstract class OnSwooleEventBase extends AnnotationBase implements Level, Rule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Required
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/** @var string */
|
||||
public $rule = "";
|
||||
|
||||
/** @var int */
|
||||
/**
|
||||
* @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;
|
||||
return $this->rule !== "" ? $this->rule : true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +46,4 @@ class SwooleEventAfter extends AnnotationBase implements Rule, 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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,30 +5,73 @@ namespace ZM\Event;
|
||||
|
||||
|
||||
use Doctrine\Common\Annotations\AnnotationException;
|
||||
use Error;
|
||||
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
|
||||
{
|
||||
const STATUS_NORMAL = 0; //正常结束
|
||||
const STATUS_INTERRUPTED = 1; //被interrupt了,不管在什么地方
|
||||
const STATUS_EXCEPTION = 2; //执行过程中抛出了异常
|
||||
const STATUS_BEFORE_FAILED = 3; //中间件HandleBefore返回了false,所以不执行此方法
|
||||
const STATUS_RULE_FAILED = 4; //判断事件执行的规则函数判定为false,所以不执行此方法
|
||||
|
||||
/** @var string */
|
||||
private $class;
|
||||
/** @var null|callable */
|
||||
private $rule = null;
|
||||
/** @var null|callable */
|
||||
private $return_func = null;
|
||||
/** @var bool */
|
||||
private $log = false;
|
||||
/** @var int */
|
||||
private $eid = 0;
|
||||
/** @var int */
|
||||
public $status = self::STATUS_NORMAL;
|
||||
/** @var mixed */
|
||||
public $store = null;
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
@@ -41,35 +84,56 @@ class EventDispatcher
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed ...$params
|
||||
* @throws Exception
|
||||
* @throws InterruptException
|
||||
*/
|
||||
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);
|
||||
$this->dispatchEvent($v, $this->rule, ...$params);
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
|
||||
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
|
||||
if (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
|
||||
($this->return_func)($this->store);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
|
||||
} catch (InterruptException $e) {
|
||||
return null;
|
||||
} catch (AnnotationException $e) {
|
||||
return false;
|
||||
$this->store = $e->return_var;
|
||||
$this->status = self::STATUS_INTERRUPTED;
|
||||
} catch (Exception $e) {
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
throw $e;
|
||||
} catch (Error $e) {
|
||||
$this->status = self::STATUS_EXCEPTION;
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AnnotationBase|null $v
|
||||
* @param mixed $v
|
||||
* @param null $rule_func
|
||||
* @param mixed ...$params
|
||||
* @return bool
|
||||
* @throws AnnotationException
|
||||
* @throws InterruptException
|
||||
* @return bool
|
||||
*/
|
||||
public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) {
|
||||
public function dispatchEvent($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 . " 方法下的 ruleFunc ...");
|
||||
if ($rule_func !== null && !$rule_func($v)) {
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 判断为 false, 拒绝执行此方法。");
|
||||
$this->status = self::STATUS_RULE_FAILED;
|
||||
return false;
|
||||
}
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] " . $q_c . "::" . $q_f . " 方法下的 ruleFunc 为真,继续执行方法本身 ...");
|
||||
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 +145,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);
|
||||
$result = $q_o->$q_f(...$params);
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
|
||||
$this->store = $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,15 +183,22 @@ 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;
|
||||
$this->status = self::STATUS_NORMAL;
|
||||
return true;
|
||||
}
|
||||
$this->status = self::STATUS_BEFORE_FAILED;
|
||||
return false;
|
||||
} else {
|
||||
$q_o = ZMUtil::getModInstance($q_c);
|
||||
return $q_o->$q_f(...$params);
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 正在执行方法 " . $q_c . "::" . $q_f . " ...");
|
||||
$this->store = $q_o->$q_f(...$params);
|
||||
$this->status = self::STATUS_NORMAL;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class EventManager
|
||||
|
||||
public static function addEvent($event_name, ?AnnotationBase $event_obj) {
|
||||
self::$events[$event_name][] = $event_obj;
|
||||
(new AnnotationParser())->sortByLevel(self::$events, $event_name);
|
||||
}
|
||||
|
||||
public static function loadEventByParser(AnnotationParser $parser) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use Error;
|
||||
use Exception;
|
||||
use PDO;
|
||||
use ReflectionException;
|
||||
use Swoole\Coroutine;
|
||||
use Swoole\Database\PDOConfig;
|
||||
use Swoole\Database\PDOPool;
|
||||
use Swoole\Event;
|
||||
@@ -16,7 +17,11 @@ use Swoole\Process;
|
||||
use Swoole\Timer;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Annotation\Swoole\OnWorkerStart;
|
||||
use ZM\Annotation\Swoole\OnCloseEvent;
|
||||
use ZM\Annotation\Swoole\OnMessageEvent;
|
||||
use ZM\Annotation\Swoole\OnOpenEvent;
|
||||
use ZM\Annotation\Swoole\OnRequestEvent;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Annotation\Swoole\OnSwooleEvent;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
@@ -24,7 +29,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 +38,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 +50,7 @@ use ZM\Utils\ZMUtil;
|
||||
class ServerEventHandler
|
||||
{
|
||||
/**
|
||||
* @HandleEvent("start")
|
||||
* @SwooleHandler("start")
|
||||
*/
|
||||
public function onStart() {
|
||||
global $terminal_id;
|
||||
@@ -69,6 +75,40 @@ class ServerEventHandler
|
||||
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
|
||||
ZMUtil::stop();
|
||||
});
|
||||
set_error_handler(function ($error_no, $error_msg, $error_file, $error_line) {
|
||||
switch ($error_no) {
|
||||
case E_WARNING:
|
||||
$level_tips = 'PHP Warning: ';
|
||||
break;
|
||||
case E_NOTICE:
|
||||
$level_tips = 'PHP Notice: ';
|
||||
break;
|
||||
case E_DEPRECATED:
|
||||
$level_tips = 'PHP Deprecated: ';
|
||||
break;
|
||||
case E_USER_ERROR:
|
||||
$level_tips = 'User Error: ';
|
||||
break;
|
||||
case E_USER_WARNING:
|
||||
$level_tips = 'User Warning: ';
|
||||
break;
|
||||
case E_USER_NOTICE:
|
||||
$level_tips = 'User Notice: ';
|
||||
break;
|
||||
case E_USER_DEPRECATED:
|
||||
$level_tips = 'User Deprecated: ';
|
||||
break;
|
||||
case E_STRICT:
|
||||
$level_tips = 'PHP Strict: ';
|
||||
break;
|
||||
default:
|
||||
$level_tips = 'Unkonw Type Error: ';
|
||||
break;
|
||||
} // do some handle
|
||||
$error = $level_tips . $error_msg . ' in ' . $error_file . ' on ' . $error_line;
|
||||
Console::warning($error); // 如果 return false 则错误会继续递交给 PHP 标准错误处理 /
|
||||
return true;
|
||||
}, E_ALL | E_STRICT);
|
||||
if (Framework::$argv["watch"]) {
|
||||
if (extension_loaded('inotify')) {
|
||||
Console::warning("Enabled File watcher, do not use in production.");
|
||||
@@ -87,14 +127,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 +143,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("WorkerStart")
|
||||
* @SwooleHandler("WorkerStart")
|
||||
* @param Server $server
|
||||
* @param $worker_id
|
||||
*/
|
||||
@@ -113,7 +153,7 @@ class ServerEventHandler
|
||||
Console::debug("正在关闭 " . ($server->taskworker ? "Task" : "") . "Worker 进程 " . Console::setColor("#" . \server()->worker_id, "gold") . TermColor::frontColor256(59) . ", pid=" . posix_getpid());
|
||||
server()->stop($worker_id);
|
||||
});
|
||||
unset(Context::$context[Co::getCid()]);
|
||||
unset(Context::$context[Coroutine::getCid()]);
|
||||
if ($server->taskworker === false) {
|
||||
try {
|
||||
register_shutdown_function(function () use ($server) {
|
||||
@@ -193,7 +233,7 @@ class ServerEventHandler
|
||||
|
||||
// 开箱即用的Redis
|
||||
$redis = ZMConfig::get("global", "redis_config");
|
||||
if($redis !== null && $redis["host"] != "") {
|
||||
if ($redis !== null && $redis["host"] != "") {
|
||||
if (!extension_loaded("redis")) Console::error("Can not find redis extension.\n");
|
||||
else ZMRedisPool::init($redis);
|
||||
}
|
||||
@@ -205,12 +245,13 @@ 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 执行完毕");
|
||||
if ($dispatcher->status === EventDispatcher::STATUS_NORMAL) Console::debug("@OnStart 执行完毕");
|
||||
else Console::warning("@OnStart 执行异常!");
|
||||
} catch (Exception $e) {
|
||||
Console::error("Worker加载出错!停止服务!");
|
||||
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
|
||||
@@ -243,16 +284,22 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("message")
|
||||
* @SwooleHandler("message")
|
||||
* @param $server
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage($server, Frame $frame) {
|
||||
|
||||
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd.": ".TermColor::ITALIC.$frame->data.TermColor::RESET);
|
||||
unset(Context::$context[Co::getCid()]);
|
||||
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd . ": " . TermColor::ITALIC . $frame->data . TermColor::RESET);
|
||||
unset(Context::$context[\Swoole\Coroutine::getCid()]);
|
||||
$conn = ManagerGM::get($frame->fd);
|
||||
set_coroutine_params(["server" => $server, "frame" => $frame, "connection" => $conn]);
|
||||
$dispatcher1 = new EventDispatcher(OnMessageEvent::class);
|
||||
$dispatcher1->setRuleFunction(function ($v) {
|
||||
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
|
||||
});
|
||||
|
||||
|
||||
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -266,9 +313,10 @@ class ServerEventHandler
|
||||
}
|
||||
});
|
||||
try {
|
||||
$starttime = microtime(true);
|
||||
//$starttime = microtime(true);
|
||||
$dispatcher1->dispatchEvents($conn);
|
||||
$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 +330,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("request")
|
||||
* @SwooleHandler("request")
|
||||
* @param $request
|
||||
* @param $response
|
||||
*/
|
||||
@@ -292,6 +340,11 @@ class ServerEventHandler
|
||||
Console::debug("Calling Swoole \"request\" event from fd=" . $request->fd);
|
||||
set_coroutine_params(["request" => $request, "response" => $response]);
|
||||
|
||||
$dis1 = new EventDispatcher(OnRequestEvent::class);
|
||||
$dis1->setRuleFunction(function ($v) {
|
||||
return eval("return " . $v->getRule() . ";") ? true : false;
|
||||
});
|
||||
|
||||
$dis = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dis->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -304,8 +357,10 @@ class ServerEventHandler
|
||||
});
|
||||
|
||||
try {
|
||||
$no_interrupt = $dis->dispatchEvents($request, $response);
|
||||
if ($no_interrupt !== null) {
|
||||
$dis1->dispatchEvents($request, $response);
|
||||
$dis->dispatchEvents($request, $response);
|
||||
var_dump($dis->status);
|
||||
if ($dis->status === EventDispatcher::STATUS_NORMAL && $dis1->status === EventDispatcher::STATUS_NORMAL) {
|
||||
$result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params);
|
||||
if ($result === true) {
|
||||
ctx()->setCache("params", $params);
|
||||
@@ -317,8 +372,8 @@ class ServerEventHandler
|
||||
$div->request_method = $node["request_method"];
|
||||
$div->class = $node["class"];
|
||||
//Console::success("正在执行路由:".$node["method"]);
|
||||
$r = $dispatcher->dispatchEvent($div, null, $params, $request, $response);
|
||||
if (is_string($r) && !$response->isEnd()) $response->end($r);
|
||||
$dispatcher->dispatchEvent($div, null, $params, $request, $response);
|
||||
if (is_string($dispatcher->store) && !$response->isEnd()) $response->end($dispatcher->store);
|
||||
}
|
||||
}
|
||||
if (!$response->isEnd()) {
|
||||
@@ -336,7 +391,7 @@ class ServerEventHandler
|
||||
else
|
||||
$response->end("Internal server error.");
|
||||
}
|
||||
Console::error("Internal server exception (500), caused by " . get_class($e).": ".$e->getMessage());
|
||||
Console::error("Internal server exception (500), caused by " . get_class($e) . ": " . $e->getMessage());
|
||||
Console::log($e->getTraceAsString(), "gray");
|
||||
} catch (Error $e) {
|
||||
$response->status(500);
|
||||
@@ -350,13 +405,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
|
||||
*/
|
||||
@@ -368,7 +423,13 @@ class ServerEventHandler
|
||||
ManagerGM::pushConnect($request->fd, $type_conn);
|
||||
$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"]) ?? "");
|
||||
$conn->setOption("connect_id", strval($request->header["x-self-id"] ?? ""));
|
||||
|
||||
$dispatcher1 = new EventDispatcher(OnOpenEvent::class);
|
||||
$dispatcher1->setRuleFunction(function ($v) {
|
||||
return ctx()->getConnection()->getName() == $v->connect_type && eval("return " . $v->getRule() . ";");
|
||||
});
|
||||
|
||||
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -380,6 +441,12 @@ 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);
|
||||
}
|
||||
}
|
||||
$dispatcher1->dispatchEvents($conn);
|
||||
$dispatcher->dispatchEvents($conn);
|
||||
} catch (Exception $e) {
|
||||
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
|
||||
@@ -394,7 +461,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("close")
|
||||
* @SwooleHandler("close")
|
||||
* @param $server
|
||||
* @param $fd
|
||||
*/
|
||||
@@ -404,6 +471,12 @@ class ServerEventHandler
|
||||
if ($conn === null) return;
|
||||
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
|
||||
set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]);
|
||||
|
||||
$dispatcher1 = new EventDispatcher(OnCloseEvent::class);
|
||||
$dispatcher1->setRuleFunction(function ($v) {
|
||||
return $v->connect_type == ctx()->getConnection()->getName() && eval("return " . $v->getRule() . ";");
|
||||
});
|
||||
|
||||
$dispatcher = new EventDispatcher(OnSwooleEvent::class);
|
||||
$dispatcher->setRuleFunction(function ($v) {
|
||||
if ($v->getRule() == '') {
|
||||
@@ -415,6 +488,12 @@ 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);
|
||||
}
|
||||
}
|
||||
$dispatcher1->dispatchEvents($conn);
|
||||
$dispatcher->dispatchEvents($conn);
|
||||
} catch (Exception $e) {
|
||||
$error_msg = $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")";
|
||||
@@ -429,7 +508,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("pipeMessage")
|
||||
* @SwooleHandler("pipeMessage")
|
||||
* @param $server
|
||||
* @param $src_worker_id
|
||||
* @param $data
|
||||
@@ -462,7 +541,7 @@ class ServerEventHandler
|
||||
}
|
||||
|
||||
/**
|
||||
* @HandleEvent("task")
|
||||
* @SwooleHandler("task")
|
||||
*/
|
||||
public function onTask() {
|
||||
}
|
||||
@@ -491,7 +570,16 @@ class ServerEventHandler
|
||||
//加载各个模块的注解类,以及反射
|
||||
Console::debug("检索Module中");
|
||||
$parser = new AnnotationParser();
|
||||
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/Module/", "Module");
|
||||
$path = DataProvider::getWorkingDir() . "/src/";
|
||||
$dir = scandir($path);
|
||||
unset($dir[0], $dir[1]);
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
|
||||
foreach ($dir as $v) {
|
||||
if (is_dir($path . "/" . $v) && isset($composer["autoload"]["psr-4"][$v . "\\"]) && !in_array($composer["autoload"]["psr-4"][$v . "\\"], $composer["extra"]["exclude_annotate"] ?? [])) {
|
||||
Console::verbose("Add " . $v . " to register path");
|
||||
$parser->addRegisterPath(DataProvider::getWorkingDir() . "/src/" . $v . "/", $v);
|
||||
}
|
||||
}
|
||||
$parser->registerMods();
|
||||
EventManager::loadEventByParser($parser); //加载事件
|
||||
|
||||
@@ -504,20 +592,24 @@ 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, "message_level" => 99999];
|
||||
|
||||
if ($plugins["onebot"]) {
|
||||
$obj = new OnSwooleEvent();
|
||||
$obj->class = QQBot::class;
|
||||
$obj->method = 'handle';
|
||||
$obj->type = 'message';
|
||||
$obj->level = 99999;
|
||||
$obj->level = $plugins["onebot"]["message_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: 编写加载外部插件的方式
|
||||
$this->loadExternalModules($plugins);
|
||||
}
|
||||
|
||||
private function addWatcher($maindir, $fd) {
|
||||
@@ -531,11 +623,4 @@ class ServerEventHandler
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function loadExternalModules($plugins) {
|
||||
foreach ($plugins as $k => $v) {
|
||||
if ($k == "onebot") continue;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +47,17 @@ 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 {
|
||||
$sw = ZMConfig::get("global");
|
||||
if (!is_dir($sw["zm_data"])) mkdir($sw["zm_data"]);
|
||||
if (!is_dir($sw["config_dir"])) mkdir($sw["config_dir"]);
|
||||
if (!is_dir($sw["crash_dir"])) mkdir($sw["crash_dir"]);
|
||||
ManagerGM::init(ZMConfig::get("global", "swoole")["max_connection"] ?? 2048, 0.5, [
|
||||
[
|
||||
"key" => "connect_id",
|
||||
@@ -68,7 +76,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 +120,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 +136,10 @@ class Framework
|
||||
}
|
||||
}
|
||||
|
||||
public function start() {
|
||||
self::$server->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全局配置文件里读取注入系统事件的类
|
||||
* @throws ReflectionException
|
||||
@@ -148,11 +159,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();
|
||||
@@ -163,11 +174,9 @@ class Framework
|
||||
}
|
||||
}
|
||||
foreach ($event_list as $k => $v) {
|
||||
self::$server->on($k, function (...$param) use ($v) {
|
||||
$c = ZMUtil::getModInstance($v->class);
|
||||
$m = $v->method;
|
||||
$c->$m(...$param);
|
||||
});
|
||||
$c = ZMUtil::getModInstance($v->class);
|
||||
$m = $v->method;
|
||||
self::$server->on($k, function (...$param) use ($c, $m) { $c->$m(...$param); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,14 +222,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
|
||||
{
|
||||
@@ -36,7 +35,10 @@ class QQBot
|
||||
set_coroutine_params(["data" => $data]);
|
||||
ctx()->setCache("level", 0);
|
||||
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
|
||||
$this->dispatchBeforeEvents($data); // >= 200 的level before在这里执行
|
||||
if ($data["post_type"] != "meta_event") {
|
||||
$r = $this->dispatchBeforeEvents($data); // before在这里执行,元事件不执行before为减少不必要的调试日志
|
||||
if ($r->store === "block") EventDispatcher::interrupt();
|
||||
}
|
||||
if (CoMessage::resumeByWS()) {
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
@@ -50,19 +52,27 @@ class QQBot
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @return EventDispatcher
|
||||
* @throws InterruptException
|
||||
*/
|
||||
public function dispatchBeforeEvents($data) {
|
||||
$before = new EventDispatcher(CQBefore::class);
|
||||
$before->setRuleFunction(function ($v) use ($data) {
|
||||
if ($v->level < 200) EventDispatcher::interrupt();
|
||||
elseif ($v->cq_event != $data["post_type"]) return false;
|
||||
return true;
|
||||
return $v->cq_event == $data["post_type"];
|
||||
});
|
||||
$before->setReturnFunction(function ($result) {
|
||||
if (!$result) EventDispatcher::interrupt();
|
||||
if (!$result) EventDispatcher::interrupt("block");
|
||||
});
|
||||
$before->dispatchEvents($data);
|
||||
return $before;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $data
|
||||
* @throws InterruptException
|
||||
*/
|
||||
private function dispatchEvents($data) {
|
||||
//Console::warning("最xia数据包:".json_encode($data));
|
||||
switch ($data["post_type"]) {
|
||||
@@ -76,26 +86,35 @@ class QQBot
|
||||
$word[$k] = trim($word[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
//分发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)) {
|
||||
if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
|
||||
array_shift($word);
|
||||
ctx()->setCache("match", $word);
|
||||
return true;
|
||||
} 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) {
|
||||
if ($match !== false) {
|
||||
ctx()->setCache("match", $match);
|
||||
return true;
|
||||
}
|
||||
} elseif ($v->regex != "") {
|
||||
if(preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) {
|
||||
if (preg_match("/" . $v->regex . "/u", ctx()->getMessage(), $word2) != 0) {
|
||||
ctx()->setCache("match", $word2);
|
||||
return true;
|
||||
}
|
||||
@@ -105,10 +124,10 @@ class QQBot
|
||||
});
|
||||
$dispatcher->setReturnFunction(function ($result) {
|
||||
if (is_string($result)) ctx()->reply($result);
|
||||
EventDispatcher::interrupt();
|
||||
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
|
||||
});
|
||||
$r = $dispatcher->dispatchEvents();
|
||||
if ($r === null) EventDispatcher::interrupt();
|
||||
$dispatcher->dispatchEvents();
|
||||
if ($dispatcher->status == EventDispatcher::STATUS_INTERRUPTED) EventDispatcher::interrupt();
|
||||
|
||||
//分发CQMessage事件
|
||||
$msg_dispatcher = new EventDispatcher(CQMessage::class);
|
||||
@@ -128,8 +147,7 @@ class QQBot
|
||||
//Console::success("当前数据包:".json_encode(ctx()->getData()));
|
||||
$dispatcher = new EventDispatcher(CQMetaEvent::class);
|
||||
$dispatcher->setRuleFunction(function (CQMetaEvent $v) {
|
||||
return ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == ctx()->getData()["meta_event_type"])) &&
|
||||
($v->sub_type == '' || ($v->sub_type != '' && $v->sub_type == (ctx()->getData()["sub_type"] ?? '')));
|
||||
return ($v->meta_event_type == '' || ($v->meta_event_type != '' && $v->meta_event_type == ctx()->getData()["meta_event_type"]));
|
||||
});
|
||||
//eval(BP);
|
||||
$dispatcher->dispatchEvents(ctx()->getData());
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
<?php
|
||||
<?php #plain
|
||||
|
||||
use ZM\Config\ZMConfig;
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?php
|
||||
<?php #plain
|
||||
|
||||
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) {
|
||||
@@ -84,6 +88,7 @@ function getAllClasses($dir, $indoor_name) {
|
||||
//echo "At " . $indoor_name . PHP_EOL;
|
||||
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
|
||||
elseif (mb_substr($v, -4) == ".php") {
|
||||
if(substr(file_get_contents($dir.$v), 6, 6) == "#plain") continue;
|
||||
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
|
||||
$classes [] = $class_name;
|
||||
}
|
||||
@@ -181,11 +186,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 +232,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 +273,32 @@ 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("没有任何机器人连接到框架!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取同类型所有连接的文件描述符 ID
|
||||
* @param string $type
|
||||
* @return array
|
||||
* @author 854854321
|
||||
*/
|
||||
function getAllFdByConnectType(string $type = 'default'): array {
|
||||
$fds = [];
|
||||
foreach (ManagerGM::getAllByName($type) as $obj) {
|
||||
$fds[] = $obj->getFd();
|
||||
}
|
||||
return $fds;
|
||||
}
|
||||
|
||||
@@ -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