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

|
||||

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

|
||||
|
||||
12
bin/start
12
bin/start
@@ -1,14 +1,6 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
<?php /** @noinspection PhpIncludeInspection */
|
||||
|
||||
if (!is_dir(__DIR__ . '/../vendor')) {
|
||||
define("LOAD_MODE", 1); //composer项目模式
|
||||
define("LOAD_MODE_COMPOSER_PATH", getcwd());
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
|
||||
} else {
|
||||
define("LOAD_MODE", 0); //源码模式
|
||||
require_once __DIR__ . "/../vendor/autoload.php";
|
||||
}
|
||||
require_once ((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . "/..")) . "/vendor/autoload.php";
|
||||
|
||||
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "zhamao/framework",
|
||||
"description": "High performance QQ robot and web server development framework",
|
||||
"description": "High performance chat robot and web server development framework",
|
||||
"minimum-stability": "stable",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.2.4",
|
||||
"version": "2.2.9",
|
||||
"extra": {
|
||||
"exclude_annotate": [
|
||||
"src/ZM"
|
||||
@@ -11,12 +11,8 @@
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "whale",
|
||||
"email": "crazysnowcc@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "swift",
|
||||
"email": "hugo_swift@yahoo.com"
|
||||
"name": "jerry",
|
||||
"email": "admin@zhamao.me"
|
||||
}
|
||||
],
|
||||
"prefer-stable": true,
|
||||
@@ -26,19 +22,18 @@
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"doctrine/annotations": "~1.10",
|
||||
"ext-json": "*",
|
||||
"ext-posix": "*",
|
||||
"doctrine/annotations": "~1.10",
|
||||
"psy/psysh": "@stable",
|
||||
"symfony/polyfill-ctype": "^1.20",
|
||||
"symfony/polyfill-mbstring": "^1.20",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/routing": "^5.1",
|
||||
"zhamao/connection-manager": "*@dev",
|
||||
"zhamao/console": "^1.0",
|
||||
"zhamao/config": "^1.0",
|
||||
"zhamao/request": "*@dev",
|
||||
"symfony/routing": "^5.1",
|
||||
"symfony/polyfill-php80": "^1.20",
|
||||
"ext-posix": "*"
|
||||
"zhamao/request": "*@dev"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "*",
|
||||
|
||||
@@ -27,7 +27,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/';
|
||||
/** 对应swoole的server->set参数 */
|
||||
$config['swoole'] = [
|
||||
'log_file' => $config['crash_dir'] . 'swoole_error.log',
|
||||
'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量
|
||||
//'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算,则可把这里改为1使用全局变量
|
||||
'dispatch_mode' => 2, //包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
|
||||
'max_coroutine' => 300000,
|
||||
//'task_worker_num' => 4,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# FAQ
|
||||
|
||||
这里会写一些常见的疑难解答。
|
||||
231
docs/advanced/example/admin.md
Normal file
231
docs/advanced/example/admin.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 编写管理员专属功能
|
||||
|
||||
众所周知,如果大家使用炸毛框架来开发聊天机器人的话,会比较方便。但是有些地方你一定会感觉还是欠缺了点,比如下面这样,你想编写一个只能由机器人管理员,也就是你自己,才能触发的功能:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
```
|
||||
|
||||
这时候,如果只是自己有绝对的权利,可以将自己的 QQ 号写死在注解 `@CQCommand` 中,并限定 `user_id`(假设我的 QQ 号码为 123456):
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group",user_id=123456)
|
||||
*/
|
||||
```
|
||||
|
||||
但是,随着时间的推移,你的机器人伙伴群可能越来越大,这个命令可能不止需要绝对的你来使用,你还要将机器人的部分权利下发给更多的伙伴,怎么办呢?注解里面只能写死的。
|
||||
|
||||
答案很简单,这时候我们就需要用到框架提供的中间件(Middleware)。中间件说白了就是在事件执行前、后、过程中抛出的异常对其进行阻断和插入代码,比如我们上方在触发禁言这个注解事件前首先要判断执行这个命令的是不是钦定的管理员。
|
||||
|
||||
## 第一步:定义中间件
|
||||
|
||||
首先,我们需要定义一个中间件。在框架默认提供的脚手架中,包含了一个叫 `TimerMiddleware.php` 的示例中间件,这个示例中间件的目的是非常简单的,就是判断这个注解事件运行了多长时间。假设你有一个机器人功能,这个功能下的代码需要执行很长时间,可以使用这一注解轻松将事件执行的时间打印到终端上。
|
||||
|
||||
关于中间件的有关说明,见 [中间件](/event/middleware)。
|
||||
|
||||
下面我们假设你已经阅读过中间件注解的文档了,我们着手编写一个判断指令执行者是否是指定的管理员 QQ 的中间件。为了省事和让大家方便地复现,我先在脚手架下的目录 `src/Module/Middleware/` 下新建 PHP 类文件 `AdminMiddleware.php`(和 `TimerMiddleware.php` 在同一个目录)。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Middleware;
|
||||
|
||||
use ZM\Annotation\Http\HandleBefore;
|
||||
use ZM\Annotation\Http\MiddlewareClass;
|
||||
use ZM\Exception\ZMException;
|
||||
use ZM\Http\MiddlewareInterface;
|
||||
use ZM\Store\LightCache;
|
||||
|
||||
/**
|
||||
* Class AdminMiddleware
|
||||
* 示例中间件:用于动态管理一些管理员指令的中间件
|
||||
* @package Module\Middleware
|
||||
* @MiddlewareClass("admin")
|
||||
*/
|
||||
class AdminMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @HandleBefore()
|
||||
* @return bool
|
||||
* @throws ZMException
|
||||
*/
|
||||
public function onBefore(): bool {
|
||||
$r = ctx()->getUserId(); // 从上下文获取发消息的用户 QQ
|
||||
$admin_list = LightCache::get("admin_list") ?? []; // 从轻量缓存获取管理员列表
|
||||
return in_array($r, $admin_list); // 返回这个 QQ 是否在管理员列表中
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中,`@MiddlewareClass("admin")` 的意思是,定义这个类为名字叫 `admin` 的中间件,同时,所有中间件的类**必须**带上 `implements MiddlewareInterface`,统一接口形式。
|
||||
|
||||
`@HandleBefore()` 代表的是,这个类下的这个函数(onBefore)被标注为这个中间件的 `onBefore` 事件,也就是说,如果有别的注解事件插入了这个 `admin` 中间件,那么执行对应注解事件前都要执行一下 `@HandleBefore` 所绑定的这个函数。而这个绑定的函数只能返回 `bool` 类型的值哦!
|
||||
|
||||
## 第二步:使用中间件
|
||||
|
||||
使用中间件很简单,在需要阻断的注解事件绑定的函数上再加一个注解就好了!我们以上方的禁言例子说明:
|
||||
|
||||
```php
|
||||
/**
|
||||
* @Middleware("admin")
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我是管理员
|
||||
) 禁言 1234567 600
|
||||
( 禁言成功!
|
||||
^ 假设我不在管理员名单里
|
||||
) 禁言 1234567 900
|
||||
^ 机器人没有回复,因为中间件返回了 false,不继续执行
|
||||
</chat-box>
|
||||
|
||||
而这时候有朋友又要问了,如果我有一系列管理员命令,假设都在一个叫 `AdminFunc.php` 的模块类里,我是不是还得一个一个地给注解事件写 `@Middleware("admin")` 呢?当然不需要!如果你这个类所有的注解事件都是机器人的聊天事件(`@CQCommand`,`@CQMessage`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
// ...这里是你的一堆注解事件的函数
|
||||
}
|
||||
```
|
||||
|
||||
## 第三步:补全代码
|
||||
|
||||
上面我们讲到了,中间件里面使用了 `LightCache` 轻量缓存来储存临时的管理员列表,那么我们将这部分的代码完善吧!
|
||||
|
||||
=== "src/Module/Example/AdminFunc.php"
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\API\CQ;
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="解除禁言",message_type="group")
|
||||
*/
|
||||
public function unbanSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0);
|
||||
return "解除禁言成功!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "src/Module/Example/AdminManager.php"
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
|
||||
class AdminManager
|
||||
{
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
if (!LightCache::isset("admin_list")) { //一次性代码,首次执行才会执行if
|
||||
LightCache::set("admin_list", [ // 框架启动时初始化管理员列表
|
||||
"123456",
|
||||
"234567"
|
||||
], -2); // 这里用 -2 的原因是将这一列表持久化保存,避免关闭框架后丢失
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="添加管理员")
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
public function addAdmin() { //只有管理员才能添加管理员
|
||||
$qq = ctx()->getNextArg("请输入要添加管理员的QQ(qq号码,不可at)");
|
||||
SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
$ls = LightCache::get("admin_list");
|
||||
if (!in_array($qq, $ls)) $ls[] = $qq;
|
||||
LightCache::set("admin_list", $ls, -2);
|
||||
SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
return "成功添加 $qq 到管理员列表!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 现在我是 123456
|
||||
) 禁言 13579 60
|
||||
( 禁言成功!
|
||||
) 解除禁言 13579
|
||||
( 解除禁言成功!
|
||||
) 添加管理员 98765
|
||||
( 成功添加 98765 到管理员列表!
|
||||
^ 现在我是98765
|
||||
) 禁言 13579
|
||||
( 请输入禁言的时间(秒)
|
||||
) 120
|
||||
( 禁言成功!
|
||||
</chat-box>
|
||||
|
||||
59
docs/component/access-token.md
Normal file
59
docs/component/access-token.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Token 验证
|
||||
|
||||
为了保障安全,框架支持给接入的 WebSocket 连接验证 Token,如果不设置 Token 同时又将框架的端口暴露在公网将会非常危险。
|
||||
|
||||
炸毛框架兼容 OneBot 标准的机器人客户端,所以自带一个 Token 验证器。
|
||||
|
||||
关于 Access Token 方面的标准规范,请参考下面内容:
|
||||
|
||||
- [OneBot - 鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md)
|
||||
- [go-cqhttp - 配置](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)
|
||||
|
||||
> 以 go-cqhttp 举例,如果要设置验证,则将 go-cqhttp 配置文件中的 `access_token` 项填入内容即可。
|
||||
|
||||
## 验证位置
|
||||
|
||||
框架对 Token 的验证是内置的,在事件 `open`(WebSocket 连接接入时)触发。
|
||||
|
||||
如果是兼容 OneBot 标准的客户端接入,则一切都是兼容的。
|
||||
|
||||
如果是自定义的其他 WebSocket 客户端也想接入框架,那么其他 WebSocket 客户端也需要进行相应的设置才能利用此 Token 验证。
|
||||
|
||||
如果验证成功(Token 符合要求)则分发事件 `@OnOpenEvent`,否则此事件不触发,同时断开 WebSocket 连接。
|
||||
|
||||
## 标准验证(字符串形式)
|
||||
|
||||
默认的情况下,在框架的全局配置文件 `global.php` 中,对配置项 `access_token` 填入与 OneBot 客户端相同的 `access_token` 即可实现鉴权。下面是一个最基本的和 go-cqhttp 设置鉴权配置:
|
||||
|
||||
go-cqhttp 的配置段:
|
||||
|
||||
```hjson
|
||||
// 访问密钥, 强烈推荐在公网的服务器设置
|
||||
access_token: "emhhbWFvLXJvYm90"
|
||||
```
|
||||
|
||||
框架的配置文件配置段:
|
||||
|
||||
```php
|
||||
/** onebot连接约定的token */
|
||||
$config["access_token"] = 'emhhbWFvLXJvYm90';
|
||||
```
|
||||
|
||||
然后重启框架和 go-cqhttp 即可。(其他 OneBot 客户端同理)
|
||||
|
||||
## 自定义验证(Token 验证)
|
||||
|
||||
有些情况下,使用一个单一的字符串可能无法满足你对 Token 验证的安全需求,需要自定义一些判断模式才能满足,所以框架的 `access_token` 配置项支持动态的闭包函数自行编写判断逻辑,例如下面的一个例子,我可以让框架同时允许接入多个不同 token 的 WebSocket 连接:
|
||||
|
||||
```php
|
||||
/** onebot连接约定的token */
|
||||
$config["access_token"] = function($token){
|
||||
$allow = ['emhhbWFvLXJvYm90','aXMtdmVyeS1nb29k'];
|
||||
if (in_array($token, $allow)) return true;
|
||||
else return false;
|
||||
};
|
||||
```
|
||||
|
||||
## 自定义验证(open 事件)
|
||||
|
||||
当然,这里设置了自定义方式,其实你也可以在下一层的 `@OnOpenEvent` 注解事件中进行自定义内容和判断,具体见 `@OnOpenEvent` 的相关章节。
|
||||
@@ -25,7 +25,7 @@ vendor/bin/start server --log-error # 以 error 等级启动框架
|
||||
vendor/bin/start server --log-warning # 以 warning 等级启动框架
|
||||
vendor/bin/start server --log-info # 以 info 等级启动框架
|
||||
vendor/bin/start server --log-verbose # 以 verbose 等级启动框架
|
||||
vendor/bin/start server --log-debug # 以 debug 等级 启动框架
|
||||
vendor/bin/start server --log-debug # 以 debug 等级启动框架
|
||||
```
|
||||
|
||||
## 使用 Log 输出内容
|
||||
@@ -100,11 +100,9 @@ $str = Console::setColor("I am gold color.", "gold");
|
||||
|
||||
炸毛框架支持从终端输入命令来进行一些操作,例如重启框架、停止框架、执行函数等。
|
||||
|
||||
::: warning 注意
|
||||
!!! warning 注意
|
||||
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
|
||||
:::
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
|
||||
### reload
|
||||
|
||||
@@ -160,6 +158,8 @@ color green 我是绿色的字
|
||||
|
||||
文件位置:`config/motd.txt`
|
||||
|
||||
其中,默认的 `Zhamao` 字样的 MOTD 是使用 **figlet** 命令生成的,`figlet "Zhamao"`,你也可以针对自己的机器人名称或品牌进行生成。
|
||||
|
||||
## 设置输出主题
|
||||
|
||||
Console 组件支持为多种不同的终端设置不同的主题,比如有些人喜欢使用白色的终端,但是白色终端下 info 的颜色很浅,看不到,还有人使用不能显示颜色的黑白终端.....
|
||||
|
||||
@@ -51,7 +51,7 @@ public function hello() {
|
||||
* @CQCommand("测试fd")
|
||||
*/
|
||||
public function testfd() {
|
||||
ctx()->reply("当前机器人连接的fd是:".ctx()->getFd()",机器人QQ是:".ctx()->getRobotId());
|
||||
ctx()->reply("当前机器人连接的fd是:".ctx()->getFd().",机器人QQ是:".ctx()->getRobotId());
|
||||
}
|
||||
```
|
||||
|
||||
@@ -421,4 +421,5 @@ public function argTest1() {
|
||||
<chat-box>
|
||||
) test abc 334 argtest
|
||||
( 参数内容:abc, 334, argtest
|
||||
</chat-box>
|
||||
</chat-box>
|
||||
|
||||
|
||||
@@ -82,15 +82,20 @@ class Hello {
|
||||
|
||||
CQ 码字符反转义。
|
||||
|
||||
定义:`CQ::encode($msg, $is_content = false)`
|
||||
|
||||
当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。
|
||||
|
||||
| 反转义前 | 反转义后 |
|
||||
| -------- | -------- |
|
||||
| `&` | `&` |
|
||||
| `[` | `[` |
|
||||
| `]` | `]` |
|
||||
| `,` | `,` |
|
||||
|
||||
```php
|
||||
$str = CQ::decode("[我只是一条普通的文本]");
|
||||
// 转换为 "[我只是一条普通的文本]"
|
||||
$str = CQ::decode("[CQ:at,qq=我只是一条普通的文本]");
|
||||
// 转换为 "[CQ:at,qq=我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
### CQ::encode()
|
||||
@@ -102,6 +107,14 @@ $str = CQ::encode("[CQ:我只是一条普通的文本]");
|
||||
// $str: "[CQ:我只是一条普通的文本]"
|
||||
```
|
||||
|
||||
定义:`CQ::encode($msg, $is_content = false)`
|
||||
|
||||
当 `$is_content` 为 true 时,会将 `,` 转义为 `,`。
|
||||
|
||||
### CQ::escape()
|
||||
|
||||
同 `CQ::encode()`。
|
||||
|
||||
### CQ::removeCQ()
|
||||
|
||||
去除字符串中所有的 CQ 码。
|
||||
@@ -111,6 +124,48 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
|
||||
// $str: "这是带表情的全体消息"
|
||||
```
|
||||
|
||||
### CQ::getCQ()
|
||||
|
||||
解析 CQ 码。
|
||||
|
||||
- 参数:`getCQ($msg);`:要解析出 CQ 码的消息。
|
||||
- 返回:`数组 | null`,见下表
|
||||
|
||||
| 键名 | 说明 |
|
||||
| ------ | ------------------------------------------------------------ |
|
||||
| type | CQ码类型,比如 `[CQ:at]` 中的 `at` |
|
||||
| params | 参数列表,比如 `[CQ:image,file=123.jpg,url=http://a.com/a.jpg]`,params 为 `["file" => "123","url" => "http://a.com/a.jpg"]` |
|
||||
| start | 此 CQ 码在字符串中的起始位置 |
|
||||
| end | 此 CQ 码在字符串中的结束位置 |
|
||||
|
||||
### CQ::getAllCQ()
|
||||
|
||||
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
|
||||
|
||||
```php
|
||||
CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
|
||||
/*
|
||||
[
|
||||
[
|
||||
"type" => "at",
|
||||
"params" => [
|
||||
"qq" => "123",
|
||||
],
|
||||
"start" => 0,
|
||||
"end" => 13,
|
||||
],
|
||||
[
|
||||
"type" => "at",
|
||||
"params" => [
|
||||
"qq" => "456",
|
||||
],
|
||||
"start" => 17,
|
||||
"end" => 30,
|
||||
],
|
||||
]
|
||||
*/
|
||||
```
|
||||
|
||||
## CQ 码列表
|
||||
|
||||
### CQ::face() - 发送 QQ 表情
|
||||
@@ -449,11 +504,31 @@ public function xmlTest() {
|
||||
|
||||
发送 QQ 兼容的 JSON 多媒体消息。
|
||||
|
||||
定义:`CQ::json($data)`
|
||||
定义:`CQ::json($data, $resid = 0)`
|
||||
|
||||
参数同上,内含 JSON 字符串即可。
|
||||
|
||||
其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0,走小程序通道,填了走富文本通道发送。
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
|
||||
|
||||
### CQ::_custom() - 扩展自定义 CQ 码
|
||||
|
||||
用于兼容各类含有被支持的扩展 CQ 码,比如 go-cqhttp 的 `[CQ:gift]` 礼物类型。
|
||||
|
||||
定义:`CQ::_custom(string $type_name, array $params)`
|
||||
|
||||
| 参数名 | 说明 |
|
||||
| ----------- | --------------------------------------------------- |
|
||||
| `type_name` | CQ 码类型,如 `music`,`at` |
|
||||
| `params` | 发送的 CQ 码中的参数数组,例如 `["qq" => "123456"]` |
|
||||
|
||||
下面是一个例子:
|
||||
|
||||
```php
|
||||
CQ::_custom("at",["qq" => "123456","qwe" => "asd"]);
|
||||
// 返回:[CQ:at,qq=123456,qwe=asd]
|
||||
```
|
||||
|
||||
|
||||
54
docs/component/data-provider.md
Normal file
54
docs/component/data-provider.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# 存储管理(文件)
|
||||
|
||||
DataProvider 是框架内提供的一个简易的文件管理类。
|
||||
|
||||
定义:`\ZM\Utils\DataProvider`
|
||||
|
||||
## DataProvider::getWorkingDir()
|
||||
|
||||
同 `working_dir()`。
|
||||
|
||||
## DataProvider::getFrameworkLink()
|
||||
|
||||
同 `ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
|
||||
|
||||
## DataProvider::getDataFolder()
|
||||
|
||||
获取配置项 `zm_data` 指定的目录。
|
||||
|
||||
## DataProvider::saveToJson()
|
||||
|
||||
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
|
||||
|
||||
定义:`saveToJson($filename, $file_array)`
|
||||
|
||||
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
|
||||
|
||||
这里只支持二级目录,不支持更多级的子目录。
|
||||
|
||||
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
|
||||
|
||||
## DataProvider::loadFromJson()
|
||||
|
||||
从 json 文件加载内容至变量。
|
||||
|
||||
定义:`loadFromJson($filename)`
|
||||
|
||||
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
|
||||
|
||||
## 其他文件读取
|
||||
|
||||
框架比较贴近原生的 PHP,所以推荐直接使用原生的方法来读写文件(`file_get_contents` 和 `file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。
|
||||
|
||||
```php
|
||||
// 读取框架工作目录的文件 composer.json 文件
|
||||
$r = file_get_contents(working_dir() . "/composer.json");
|
||||
|
||||
// 写入 Linux 临时目录下的文件
|
||||
file_put_contents("/tmp/test.txt", "hello world");
|
||||
```
|
||||
|
||||
!!! warning "注意"
|
||||
|
||||
在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。
|
||||
|
||||
@@ -227,5 +227,32 @@ bot()->sendPrivateMsg(123456, "你好啊!!");
|
||||
// 等同于 ZMRobot::getRandom()->sendPrivateMsg(123456, "你好啊!!");
|
||||
```
|
||||
|
||||
## zm_atomic()
|
||||
|
||||
获取计时器,效果同 `\ZM\Store\ZMAtomic::get($name)`。
|
||||
|
||||
定义:`zm_atmoic($name)`
|
||||
|
||||
## uuidgen()
|
||||
|
||||
> 2.2.5 版本起可用。
|
||||
|
||||
生成一个随机的 uuid,支持大写或小写。
|
||||
|
||||
定义:`uuidgen($uppercase = false)`
|
||||
|
||||
当 `$uppercase` 为 `true` 时,返回的 uuid 中字母都是大写。
|
||||
|
||||
## working_dir()
|
||||
|
||||
> 2.2.6 版本起可用。
|
||||
|
||||
获取框架运行的工作目录。例如你是从 `/root/framework-starter/` 目录启动的框架,`vendor/bin/start server`,那么 `working_dir()` 返回的就是 `/root/framework-starter`。(注意,返回的目录最后没有斜杠,请自行添加。)
|
||||
|
||||
## getAllFdByConnectType()
|
||||
|
||||
获取同类型的所有连接的描述符 ID。
|
||||
|
||||
定义:`getAllFdByConnectType(string $type = 'default'): array`
|
||||
|
||||
当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
|
||||
@@ -25,8 +25,8 @@
|
||||
```php
|
||||
/** 轻量字符串缓存,默认开启 */
|
||||
$config['light_cache'] = [
|
||||
'size' => 1024, //最多允许储存的条数(需要2的倍数)
|
||||
'max_strlen' => 16384, //单行字符串最大长度(需要2的倍数)
|
||||
'size' => 512, //最多允许储存的条数(需要2的倍数)
|
||||
'max_strlen' => 32768, //单行字符串最大长度(需要2的倍数)
|
||||
'hash_conflict_proportion' => 0.6, //Hash冲突率(越大越好,但是需要的内存更多)
|
||||
'persistence_path' => $config['zm_data'].'_cache.json',
|
||||
'auto_save_interval' => 900
|
||||
|
||||
@@ -31,7 +31,7 @@ SpinLock::unlock("foo");
|
||||
给信号量 `$key` 上锁。如果该信号量已经被上锁,则立刻返回 false。
|
||||
|
||||
```php
|
||||
SpinLock::lock("foo");
|
||||
SpinLock::trylock("foo");
|
||||
```
|
||||
|
||||
## 综合实例
|
||||
@@ -70,4 +70,4 @@ public function test() {
|
||||
|
||||
## 性能
|
||||
|
||||
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下,使用自旋锁和不适用自旋锁的测试成绩时间分别为:7.4s 和 6.9s。
|
||||
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下,使用自旋锁和不适用自旋锁的测试成绩时间分别为:7.4s 和 6.9s。
|
||||
|
||||
@@ -163,5 +163,5 @@ public function onThrowing(?Exception $e) {
|
||||
|
||||
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
|
||||
|
||||
`context()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。
|
||||
`ctx()` 为获取当前协程空间绑定的 `request` 和 `response` 对象。
|
||||
|
||||
|
||||
@@ -11,10 +11,25 @@ QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理
|
||||
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
|
||||
|
||||
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand),`@CQMessage` 注解处理。
|
||||
|
||||
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
|
||||
|
||||
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
|
||||
|
||||
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
|
||||
|
||||
## 注解事件参照表
|
||||
|
||||
| 注解名称 | 类所在命名全称 | 作用 |
|
||||
| ------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ |
|
||||
| [`@CQBefore`](/event/robot-annotations/#cqbefore) | `\ZM\Annotation\CQBefore` | OneBot 各类事件前触发的,可当作事件过滤器使用 |
|
||||
| [`@CQAfter`](/event/robot-annotations/#cqafter) | `\ZM\Annotation\CQAfter` | OneBot 各类事件后触发的 |
|
||||
| [`@CQMessage`](/event/robot-annotations/#cqmessage) | `\ZM\Annotation\CQMessage` | OneBot 中消息类事件的触发(机器人消息)事件 |
|
||||
| [`@CQCommand`](/event/robot-annotations/#cqcommand) | `\ZM\Annotation\CQCommand` | OneBot 中消息类事件的触发(机器人消息)事件,但是被封装为指令型的,无需自己切割命令式 |
|
||||
| [`@CQNotice`](/event/robot-annotations/#cqnotice) | `\ZM\Annotation\CQNotice` | OneBot 中通知类事件的触发(机器人消息)事件 |
|
||||
| [`@CQRequest`](/event/robot-annotations/#cqrequest) | `\ZM\Annotation\CQRequest` | OneBot 中请求类事件的触发(机器人消息)事件,一般带有请求信息,可联动相关响应的 API 完成功能编写 |
|
||||
| [`@CQMetaEvent`](/event/robot-annotations/#cqmetaevent) | `\ZM\Annotation\CQMetaEvent` | OneBot 中涉及 OneBot 实现本身的一些和机器人事件无关的元事件,比如 WS 连接的心跳包 |
|
||||
|
||||
## CQMessage()
|
||||
|
||||
QQ 收到消息后触发的事件对应注解。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
!!! quote "开发提示"
|
||||
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不罗嗦。
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不啰嗦。
|
||||
|
||||
## 控制器和路由
|
||||
|
||||
@@ -228,4 +228,4 @@ public function staticImage($param) {
|
||||
}
|
||||
```
|
||||
|
||||
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
|
||||
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
|
||||
|
||||
@@ -4,35 +4,35 @@
|
||||
|
||||
!!! error "警告"
|
||||
|
||||
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将将敏感信息写入配置文件并提交至开源仓库!
|
||||
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将敏感信息写入配置文件并提交至开源仓库!
|
||||
|
||||
## 全局配置文件 global.php
|
||||
|
||||
框架的全局配置文件在 `config/global.php` 文件中。下面是配置文件的各个选项,请根据自己的需要自行配置。
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| :--------------------------- | ------------------------------------------------ | ---------------------------- |
|
||||
| `host` | 框架监听的地址 | 0.0.0.0 |
|
||||
| `port` | 框架监听的端口 | 20001 |
|
||||
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
|
||||
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
|
||||
| `debug_mode` | 框架是否启动 debug 模式 | false |
|
||||
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
|
||||
| `swoole` | 对应 Swoole server 中 set 的参数,参考Swoole文档 | 见子表 `swoole` |
|
||||
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
|
||||
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
|
||||
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
|
||||
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
|
||||
| `access_token` | OneBot 客户端连接约定的token,留空则无 | 空 |
|
||||
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
|
||||
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
|
||||
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
|
||||
| `info_level` | 终端日志显示等级(0-4) | 2 |
|
||||
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
|
||||
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
|
||||
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
|
||||
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
|
||||
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| :--------------------------- | ------------------------------------------------------------ | ---------------------------- |
|
||||
| `host` | 框架监听的地址 | 0.0.0.0 |
|
||||
| `port` | 框架监听的端口 | 20001 |
|
||||
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
|
||||
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
|
||||
| `debug_mode` | 框架是否启动 debug 模式 | false |
|
||||
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
|
||||
| `swoole` | 对应 Swoole server 中 set 的参数,参考Swoole文档 | 见子表 `swoole` |
|
||||
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
|
||||
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
|
||||
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
|
||||
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
|
||||
| `access_token` | OneBot 客户端连接约定的token,留空则无,相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
|
||||
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
|
||||
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
|
||||
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
|
||||
| `info_level` | 终端日志显示等级(0-4) | 2 |
|
||||
| `context_class` | 上下文所定义的类,待上下文完善后见对应文档 | `\ZM\Context\Context::class` |
|
||||
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
|
||||
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表 | 见配置文件 |
|
||||
| `command_register_class` | 注册自定义命令行选项指令的类 | 见配置文件 |
|
||||
| `modules` | 服务器启用的外部第三方和内部插件 | `['onebot' => true]` |
|
||||
|
||||
### 子表 **swoole**
|
||||
|
||||
@@ -47,12 +47,18 @@
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------------------------- | ----------------------------------------------- | ---------------------------- |
|
||||
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 1024 |
|
||||
| `max_strlen` | 作为 value 字符串的最大长度 | 16384 |
|
||||
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 512 |
|
||||
| `max_strlen` | 作为 value 字符串的最大长度 | 32768 |
|
||||
| `hash_conflict_proportion` | Hash冲突率(越大越好,但是需要的内存更多) | 0.6 |
|
||||
| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` |
|
||||
| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 |
|
||||
|
||||
### 子表 worker_cache
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------- | --------------------------- | ------ |
|
||||
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
|
||||
|
||||
### 子表 **sql_config**
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
@@ -83,19 +89,13 @@
|
||||
| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` |
|
||||
| `document_index` | 默认索引的文件名列表 | `["index.html"]` |
|
||||
|
||||
### 子表 worker_cache
|
||||
|
||||
| 配置名称 | 说明 | 默认值 |
|
||||
| -------- | --------------------------- | ------ |
|
||||
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
|
||||
|
||||
## 多环境下的配置文件
|
||||
|
||||
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(production / development / staging)来编写配置文件。
|
||||
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境(development / staging / production)来编写配置文件。
|
||||
|
||||
### 使用环境参数
|
||||
|
||||
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `production`,`staging`,`development`。
|
||||
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `development`,`staging`,`production`。
|
||||
|
||||
```bash
|
||||
vendor/bin/start server --env=development
|
||||
@@ -103,7 +103,7 @@ vendor/bin/start server --env=development
|
||||
|
||||
### 不同环境配置文件
|
||||
|
||||
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制或改名为 `global.development.php` 或 `global.production.php` 即可。
|
||||
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制并重命名为 `global.development.php` 或 `global.production.php` 即可。
|
||||
|
||||
### 优先级
|
||||
|
||||
@@ -146,4 +146,4 @@ $r = ZMConfig::get("example_a", "key1"); # $r == "value1"
|
||||
$time = ZMConfig::get("example_a", "starttime"); # $time == 服务器启动时间
|
||||
```
|
||||
|
||||
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json` 或 `example_a.production.php` 均可。
|
||||
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json` 或 `example_a.production.php` 均可。
|
||||
|
||||
@@ -82,8 +82,19 @@ cd zhamao-framework-starter
|
||||
./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出
|
||||
```
|
||||
|
||||
!!! tip "提示"
|
||||
|
||||
如果国内 Composer 下载过慢,可以使用阿里云的 Composer 镜像加速。
|
||||
```bash
|
||||
# 仅对当前的项目使用阿里云加速
|
||||
composer config repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
# 对全局的 Composer 使用阿里云加速
|
||||
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
```
|
||||
|
||||
|
||||
## 启动框架
|
||||
|
||||
本地环境启动方式:
|
||||
```bash
|
||||
cd zhamao-framework-starter
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。
|
||||
|
||||
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架的。
|
||||
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架。
|
||||
|
||||
## 机器人客户端
|
||||
|
||||
@@ -30,51 +30,6 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
|
||||
|
||||
由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`。
|
||||
|
||||
=== "config.json(旧格式)"
|
||||
|
||||
``` json hl_lines="2 3 30 31"
|
||||
{
|
||||
"uin": 你的QQ号,
|
||||
"password": "你的密码",
|
||||
"encrypt_password": false,
|
||||
"password_encrypted": "",
|
||||
"enable_db": true,
|
||||
"access_token": "",
|
||||
"relogin": {
|
||||
"enabled": true,
|
||||
"relogin_delay": 3,
|
||||
"max_relogin_times": 0
|
||||
},
|
||||
"ignore_invalid_cqcode": false,
|
||||
"force_fragmented": true,
|
||||
"heartbeat_interval": 0,
|
||||
"http_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 5700,
|
||||
"timeout": 0,
|
||||
"post_urls": {}
|
||||
},
|
||||
"ws_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 6700
|
||||
},
|
||||
"ws_reverse_servers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"reverse_url": "ws://127.0.0.1:20001/",
|
||||
"reverse_api_url": "",
|
||||
"reverse_event_url": "",
|
||||
"reverse_reconnect_interval": 3000
|
||||
}
|
||||
],
|
||||
"post_message_format": "string",
|
||||
"debug": false,
|
||||
"log_level": ""
|
||||
}
|
||||
```
|
||||
|
||||
=== "config.hjson(新格式)"
|
||||
|
||||
``` json hl_lines="3 5 81 84"
|
||||
@@ -193,6 +148,51 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
|
||||
}
|
||||
```
|
||||
|
||||
=== "config.json(旧格式)"
|
||||
|
||||
``` json hl_lines="2 3 30 31"
|
||||
{
|
||||
"uin": 你的QQ号,
|
||||
"password": "你的密码",
|
||||
"encrypt_password": false,
|
||||
"password_encrypted": "",
|
||||
"enable_db": true,
|
||||
"access_token": "",
|
||||
"relogin": {
|
||||
"enabled": true,
|
||||
"relogin_delay": 3,
|
||||
"max_relogin_times": 0
|
||||
},
|
||||
"ignore_invalid_cqcode": false,
|
||||
"force_fragmented": true,
|
||||
"heartbeat_interval": 0,
|
||||
"http_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 5700,
|
||||
"timeout": 0,
|
||||
"post_urls": {}
|
||||
},
|
||||
"ws_config": {
|
||||
"enabled": false,
|
||||
"host": "0.0.0.0",
|
||||
"port": 6700
|
||||
},
|
||||
"ws_reverse_servers": [
|
||||
{
|
||||
"enabled": true,
|
||||
"reverse_url": "ws://127.0.0.1:20001/",
|
||||
"reverse_api_url": "",
|
||||
"reverse_event_url": "",
|
||||
"reverse_reconnect_interval": 3000
|
||||
}
|
||||
],
|
||||
"post_message_format": "string",
|
||||
"debug": false,
|
||||
"log_level": ""
|
||||
}
|
||||
```
|
||||
|
||||
其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT
|
||||
|
||||
## 第一次对话
|
||||
@@ -224,5 +224,14 @@ public function repeat() {
|
||||
|
||||
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。
|
||||
|
||||
<chat-box>
|
||||
) echo 你好啊
|
||||
( 你好啊
|
||||
) echo
|
||||
( 请输入你要回复的内容
|
||||
) 哦豁
|
||||
( 哦豁
|
||||
</chat-box>
|
||||
|
||||
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@
|
||||
|
||||
> 如果是从 v1.x 版本升级到 v2.x,[点我看升级指南](/advanced/to-v2/)。
|
||||
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(CQHTTP 对接),包含 websocket、http 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
!!! tip "提示"
|
||||
|
||||
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request,以此来贡献此文档!
|
||||
|
||||
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
|
||||
框架主要用途为 HTTP/WS 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
|
||||
在 HTTP 和 WebSocket 服务器上,PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
|
||||
|
||||
此外,QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快,未来也会兼容微信公众号开发者模式。
|
||||
此外,QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
|
||||
|
||||
```php
|
||||
/**
|
||||
@@ -34,9 +38,9 @@ public function index() {
|
||||
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
|
||||
|
||||
1. Linux 命令行(会跑 Linux 程序)
|
||||
2. php 7.2+ 开发环境
|
||||
3. HTTP 协议(可选)
|
||||
4. OneBot 机器人聊天接口标准(可选)
|
||||
2. php 7.2+ 开发环境(项目会持续支持最新的 PHP 版本)
|
||||
3. HTTP/WebSocket 协议
|
||||
4. OneBot 机器人聊天接口标准
|
||||
|
||||
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。
|
||||
|
||||
|
||||
@@ -1,8 +1,55 @@
|
||||
# 更新日志(v2 版本)
|
||||
|
||||
## v2.2.9
|
||||
|
||||
> 更新时间:2021.3.6
|
||||
|
||||
- 更新:`reply()` 方法传入数组则变为快速相应的 API 操作
|
||||
- 修复:在 Worker 进程下调用 `ZMUtil::reload()` 会导致一些奇怪的 bug
|
||||
- 修复:`reply()` 时会 at 私聊成员的 bug(由 go-cqhttp 导致)
|
||||
|
||||
## v2.2.8
|
||||
|
||||
> 更新时间:2021.3.2
|
||||
|
||||
- 更新:MOTD 显示的方式,更加直观和炫酷
|
||||
|
||||
## v2.2.7
|
||||
|
||||
> 更新时间:2021.2.27
|
||||
|
||||
- 修复:2.2.6 版本下 `reply()` 方法在群里调用会 at 成员的 bug
|
||||
- 修复:空 `access_token` 的情况下会无法连入的 bug
|
||||
- 修复:使用 Closure 闭包函数自行编写逻辑的判断返回 false 无法阻断连接的 bug
|
||||
|
||||
## v2.2.6
|
||||
|
||||
> 更新时间:2021.2.26
|
||||
|
||||
- 新增:`uuidgen()` 全局函数,快速生成 uuid
|
||||
- 修复:MySQL `rawQuery()` 在参数为非数组时会报 Warning 的 bug
|
||||
- 新增:示例模块的 API 示例:一言查询
|
||||
- 优化:删减部分无用代码
|
||||
- 更改:`ctx()->reply()` 方法改为调用隐藏方法:`.handle_quick_operation`
|
||||
- 修复:`ctx()->finalReply()` 一直以来的 bug(未阻断事件)
|
||||
- 新增:`access_token` 配置项支持闭包函数自行设计判断方式和逻辑
|
||||
- 新增:全局函数 `working_dir()`
|
||||
|
||||
## v2.2.5
|
||||
|
||||
> 更新时间:2021.2.20
|
||||
|
||||
- 新增:`saveToJson()` 和 `loadFromJson()` 方法(DataProvider 类)
|
||||
- 修复:`@OnSave` 注解事件无法工作的 bug
|
||||
- 调整:自定义计时器创建时的性能调优
|
||||
- 新增:WorkerCache 方法:`hasKey()`
|
||||
- 新增:SpinLock 方法:`transaction()`(直接在事务中上锁)
|
||||
- 新增:CQ 方法:`getAllCQ()`,`_custom()`(获取消息中的所有 CQ 码)
|
||||
- 修复:CQ 类中的部分 bug
|
||||
|
||||
## v2.2.4
|
||||
|
||||
> 更新事件:2021.2.7
|
||||
> 更新时间:2021.2.7
|
||||
|
||||
- 修复:终端交互导致的 ssh 断掉后 CPU 占用过高的问题
|
||||
- 修复:WorkerCache 在缺少配置文件下工作异常的问题
|
||||
|
||||
@@ -10,8 +10,8 @@ theme:
|
||||
favicon: assets/favicon.png
|
||||
language: zh
|
||||
palette:
|
||||
primary: blue
|
||||
accent: blue
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
features:
|
||||
- navigation.tabs
|
||||
extra_javascript:
|
||||
@@ -81,12 +81,14 @@ nav:
|
||||
- Redis 数据库: component/redis.md
|
||||
- ZMAtomic 原子计数器: component/atomics.md
|
||||
- SpinLock 自旋锁: component/spin-lock.md
|
||||
- 文件管理: component/data-provider.md
|
||||
- 协程池: component/coroutine-pool.md
|
||||
- 单例类: component/singleton-trait.md
|
||||
- ZMUtil 杂项: component/zmutil.md
|
||||
- 全局方法: component/global-functions.md
|
||||
- HTTP 和 WebSocket 客户端: component/zmrequest.md
|
||||
- Console 终端: component/console.md
|
||||
- Token 验证: component/access-token.md
|
||||
- 进阶开发:
|
||||
- 进阶开发: advanced/index.md
|
||||
- 框架剖析: advanced/framework-structure.md
|
||||
@@ -95,6 +97,8 @@ nav:
|
||||
- 内部类文件手册: advanced/inside-class.md
|
||||
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
|
||||
- 框架多进程: advanced/multi-process.md
|
||||
- 开发实战教程:
|
||||
- 编写管理员才能触发的功能: advanced/example/admin.md
|
||||
- FAQ: FAQ.md
|
||||
- 更新日志:
|
||||
- 更新日志(v2): update/v2.md
|
||||
|
||||
@@ -11,6 +11,7 @@ use ZM\Console\Console;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Requests\ZMRequest;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,18 @@ class Hello
|
||||
return "你好啊,我是由炸毛框架构建的机器人!";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个最基本的第三方 API 接口使用示例
|
||||
* @CQCommand("一言")
|
||||
*/
|
||||
public function hitokoto() {
|
||||
$api_result = ZMRequest::get("https://v1.hitokoto.cn/");
|
||||
if ($api_result === false) return "接口请求出错,请稍后再试!";
|
||||
$obj = json_decode($api_result, true);
|
||||
if ($obj === null) return "接口解析出错!可能返回了非法数据!";
|
||||
return $obj["hitokoto"] . "\n----「" . $obj["from"] . "」";
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个简单随机数的功能demo
|
||||
* 问法1:随机数 1 20
|
||||
@@ -89,7 +102,7 @@ class Hello
|
||||
* @return string
|
||||
*/
|
||||
public function paramGet($param) {
|
||||
return "Hello, ".$param["name"];
|
||||
return "Hello, " . $param["name"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,11 +45,11 @@ class CQ
|
||||
*/
|
||||
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:image,file=" . $file .
|
||||
"[CQ:image,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($flash ? ",type=flash" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -64,11 +64,11 @@ class CQ
|
||||
*/
|
||||
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:record,file=" . $file .
|
||||
"[CQ:record,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
($magic ? ",magic=1" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -82,10 +82,10 @@ class CQ
|
||||
*/
|
||||
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
|
||||
return
|
||||
"[CQ:video,file=" . $file .
|
||||
"[CQ:video,file=" . self::encode($file, true) .
|
||||
(!$cache ? ",cache=0" : "") .
|
||||
(!$proxy ? ",proxy=false" : "") .
|
||||
($timeout != -1 ? (",timeout=" . $timeout) : "") .
|
||||
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class CQ
|
||||
* @return string
|
||||
*/
|
||||
public static function poke($type, $id, $name = "") {
|
||||
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? ",name=$name" : "") . "]";
|
||||
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? (",name=".self::encode($name, true)) : "") . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +130,7 @@ class CQ
|
||||
* @return string
|
||||
*/
|
||||
public static function anonymous($ignore = 1) {
|
||||
return "[CQ:anonymous".($ignore != 1 ? ",ignore=0" : "")."]";
|
||||
return "[CQ:anonymous" . ($ignore != 1 ? ",ignore=0" : "") . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,10 +143,10 @@ class CQ
|
||||
*/
|
||||
public static function share($url, $title, $content = null, $image = null) {
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
else $c = ",content=" . self::encode($content, true);
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:share,url=" . $url . ",title=" . $title . $c . $i . "]";
|
||||
else $i = ",image=" . self::encode($image, true);
|
||||
return "[CQ:share,url=" . self::encode($url, true) . ",title=" . self::encode($title, true) . $c . $i . "]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -159,8 +159,21 @@ class CQ
|
||||
return "[CQ:contact,type=$type,id=$id]";
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送位置
|
||||
* @param $lat
|
||||
* @param $lon
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public static function location($lat, $lon, $title = "", $content = "") {
|
||||
|
||||
return "[CQ:location" .
|
||||
",lat=".self::encode($lat, true) .
|
||||
",lon=".self::encode($lon, true).
|
||||
($title != "" ? (",title=".self::encode($title, true)) : "") .
|
||||
($content != "" ? (",content=".self::encode($content, true)) : "") .
|
||||
"]";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,10 +206,13 @@ class CQ
|
||||
return " ";
|
||||
}
|
||||
if ($content === null) $c = "";
|
||||
else $c = ",content=" . $content;
|
||||
else $c = ",content=" . self::encode($content, true);
|
||||
if ($image === null) $i = "";
|
||||
else $i = ",image=" . $image;
|
||||
return "[CQ:music,type=custom,url=" . $id_or_url . ",audio=" . $audio . ",title=" . $title . $c . $i . "]";
|
||||
else $i = ",image=" . self::encode($image, true);
|
||||
return "[CQ:music,type=custom,url=" .
|
||||
self::encode($id_or_url, true) .
|
||||
",audio=" . self::encode($audio, true) . ",title=" . self::encode($title, true) . $c . $i .
|
||||
"]";
|
||||
default:
|
||||
Console::warning("传入的music type($type)错误!");
|
||||
return " ";
|
||||
@@ -208,19 +224,36 @@ class CQ
|
||||
}
|
||||
|
||||
public static function node($user_id, $nickname, $content) {
|
||||
return "[CQ:node,user_id=$user_id,nickname=$nickname,content=".self::escape($content)."]";
|
||||
return "[CQ:node,user_id=$user_id,nickname=".self::encode($nickname, true).",content=" . self::encode($content, true) . "]";
|
||||
}
|
||||
|
||||
public static function xml($data) {
|
||||
return "[CQ:xml,data=" . self::encode($data, true) . "]";
|
||||
}
|
||||
|
||||
public static function json($data, $resid = 0) {
|
||||
return "[CQ:json,data=" . self::encode($data, true) . ",resid=" . intval($resid) . "]";
|
||||
}
|
||||
|
||||
public static function _custom(string $type_name, $params) {
|
||||
$code = "[CQ:" . $type_name;
|
||||
foreach ($params as $k => $v) {
|
||||
$code .= "," . $k . "=" . self::escape($v, true);
|
||||
}
|
||||
$code .= "]";
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义字符串中的CQ码敏感符号
|
||||
* @param $str
|
||||
* @param $msg
|
||||
* @param bool $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function decode($str) {
|
||||
$str = str_replace("&", "&", $str);
|
||||
$str = str_replace("[", "[", $str);
|
||||
$str = str_replace("]", "]", $str);
|
||||
return $str;
|
||||
public static function decode($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
public static function replace($str) {
|
||||
@@ -230,42 +263,97 @@ class CQ
|
||||
}
|
||||
|
||||
/**
|
||||
* 转义CQ码
|
||||
* 转义CQ码的特殊字符,同encode
|
||||
* @param $msg
|
||||
* @param bool $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function escape($msg) {
|
||||
$msg = str_replace("&", "&", $msg);
|
||||
$msg = str_replace("[", "[", $msg);
|
||||
$msg = str_replace("]", "]", $msg);
|
||||
public static function escape($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
public static function encode($str) {
|
||||
return self::escape($str);
|
||||
/**
|
||||
* 转义CQ码的特殊字符
|
||||
* @param $msg
|
||||
* @param false $is_content
|
||||
* @return mixed
|
||||
*/
|
||||
public static function encode($msg, $is_content = false) {
|
||||
$msg = str_replace(["&", "[", "]"], ["&", "[", "]"], $msg);
|
||||
if ($is_content) $msg = str_replace(",", ",", $msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息中所有的CQ码并返回移除CQ码后的消息
|
||||
* @param $msg
|
||||
* @return string
|
||||
*/
|
||||
public static function removeCQ($msg) {
|
||||
while (($cq = self::getCQ($msg)) !== null) {
|
||||
$msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg);
|
||||
$final = "";
|
||||
$last_end = 0;
|
||||
foreach(self::getAllCQ($msg) as $k => $v) {
|
||||
$final .= mb_substr($msg, $last_end, $v["start"] - $last_end);
|
||||
$last_end = $v["end"] + 1;
|
||||
}
|
||||
return $msg;
|
||||
$final .= mb_substr($msg, $last_end);
|
||||
return $final;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息中第一个CQ码
|
||||
* @param $msg
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getCQ($msg) {
|
||||
if (($start = mb_strpos($msg, '[')) === false) return null;
|
||||
if (($end = mb_strpos($msg, ']')) === false) return null;
|
||||
$msg = mb_substr($msg, $start + 1, $end - $start - 1);
|
||||
if (mb_substr($msg, 0, 3) != "CQ:") return null;
|
||||
$msg = mb_substr($msg, 3);
|
||||
$msg2 = explode(",", $msg);
|
||||
$type = array_shift($msg2);
|
||||
$array = [];
|
||||
foreach ($msg2 as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$array[$sk] = implode("=", $ss);
|
||||
if (($head = mb_strpos($msg, "[CQ:")) !== false) {
|
||||
$key_offset = mb_substr($msg, $head);
|
||||
$close = mb_strpos($key_offset, "]");
|
||||
if ($close === false) return null;
|
||||
$content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg));
|
||||
$exp = explode(",", $content);
|
||||
$cq["type"] = array_shift($exp);
|
||||
foreach ($exp as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
|
||||
}
|
||||
$cq["start"] = $head;
|
||||
$cq["end"] = $close + $head;
|
||||
return $cq;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return ["type" => $type, "params" => $array, "start" => $start, "end" => $end];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息中所有的CQ码
|
||||
* @param $msg
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllCQ($msg) {
|
||||
$cqs = [];
|
||||
$offset = 0;
|
||||
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
|
||||
$key_offset = mb_substr($submsg, $head);
|
||||
$tmpmsg = mb_strpos($key_offset, "]");
|
||||
if ($tmpmsg === false) break; // 没闭合,不算CQ码
|
||||
$content = mb_substr($submsg, $head + 4, $tmpmsg + $head - mb_strlen($submsg));
|
||||
$exp = explode(",", $content);
|
||||
$cq = [];
|
||||
$cq["type"] = array_shift($exp);
|
||||
foreach ($exp as $k => $v) {
|
||||
$ss = explode("=", $v);
|
||||
$sk = array_shift($ss);
|
||||
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
|
||||
}
|
||||
$cq["start"] = $offset + $head;
|
||||
$cq["end"] = $offset + $tmpmsg + $head;
|
||||
$offset += $tmpmsg + 1;
|
||||
$cqs[] = $cq;
|
||||
}
|
||||
return $cqs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class ZMRobot
|
||||
public static function getAllRobot() {
|
||||
$r = ManagerGM::getAllByName('qq');
|
||||
$obj = [];
|
||||
foreach($r as $v) {
|
||||
foreach ($r as $v) {
|
||||
$obj[] = new ZMRobot($v);
|
||||
}
|
||||
return $obj;
|
||||
|
||||
@@ -47,7 +47,7 @@ class AnnotationParser
|
||||
*/
|
||||
public function registerMods() {
|
||||
foreach ($this->path_list as $path) {
|
||||
Console::debug("parsing annotation in ".$path[0]);
|
||||
Console::debug("parsing annotation in " . $path[0]);
|
||||
$all_class = getAllClasses($path[0], $path[1]);
|
||||
$this->reader = new AnnotationReader();
|
||||
foreach ($all_class as $v) {
|
||||
|
||||
@@ -31,7 +31,7 @@ class BuildCommand extends Command
|
||||
$target_dir = $input->getOption("target") ?? (__DIR__ . '/../../../resources/');
|
||||
if (mb_strpos($target_dir, "../")) $target_dir = realpath($target_dir);
|
||||
if ($target_dir === false) {
|
||||
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (".__DIR__ . '/../../../resources/'.")" . TermColor::RESET);
|
||||
$output->writeln(TermColor::color8(31) . "Error: No such file or directory (" . __DIR__ . '/../../../resources/' . ")" . TermColor::RESET);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$output->writeln("Target: " . $target_dir . " , Version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../../../composer.json"), true)["version"]));
|
||||
@@ -51,7 +51,7 @@ class BuildCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function build ($target_dir, $filename) {
|
||||
private function build($target_dir, $filename) {
|
||||
@unlink($target_dir . $filename);
|
||||
$phar = new Phar($target_dir . $filename);
|
||||
$phar->startBuffering();
|
||||
|
||||
@@ -18,8 +18,8 @@ class DaemonStopCommand extends DaemonCommand
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
parent::execute($input, $output);
|
||||
system("kill -TERM ".intval($this->daemon_file["pid"]));
|
||||
unlink(DataProvider::getWorkingDir()."/.daemon_pid");
|
||||
system("kill -TERM " . intval($this->daemon_file["pid"]));
|
||||
unlink(DataProvider::getWorkingDir() . "/.daemon_pid");
|
||||
$output->writeln("<info>成功停止!</info>");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ class PureHttpCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) {
|
||||
$tty_width = explode(" ", trim(exec("stty size")))[1];
|
||||
if(realpath($input->getArgument('dir') ?? '.') === false) {
|
||||
$output->writeln("<error>Directory error(".($input->getArgument('dir') ?? '.')."): no such file or directory.</error>");
|
||||
if (realpath($input->getArgument('dir') ?? '.') === false) {
|
||||
$output->writeln("<error>Directory error(" . ($input->getArgument('dir') ?? '.') . "): no such file or directory.</error>");
|
||||
return self::FAILURE;
|
||||
}
|
||||
$global = ZMConfig::get("global");
|
||||
@@ -60,15 +60,15 @@ class PureHttpCommand extends Command
|
||||
"document_root" => realpath($input->getArgument('dir') ?? '.'),
|
||||
"document_index" => $index
|
||||
]);
|
||||
echo "\r".Coroutine::stats()["coroutine_peak_num"];
|
||||
echo "\r" . Coroutine::stats()["coroutine_peak_num"];
|
||||
});
|
||||
$server->on("start", function ($server) {
|
||||
Process::signal(SIGINT, function () use ($server) {
|
||||
Console::warning("Server interrupted by keyboard.");
|
||||
for ($i = 0; $i < 32; ++$i) {
|
||||
$num = ZMAtomic::$atomics["request"][$i]->get();
|
||||
if($num != 0)
|
||||
echo "[$i]: ".$num."\n";
|
||||
if ($num != 0)
|
||||
echo "[$i]: " . $num . "\n";
|
||||
}
|
||||
$server->shutdown();
|
||||
$server->stop();
|
||||
|
||||
@@ -39,7 +39,6 @@ class RunServerCommand extends Command
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
if (LOAD_MODE == 0) echo "* This is repository mode.\n";
|
||||
(new Framework($input->getOptions()))->start();
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -26,23 +26,21 @@ class ConsoleApplication extends Application
|
||||
public function initEnv() {
|
||||
$this->selfCheck();
|
||||
|
||||
if (!is_dir(__DIR__ . '/../../vendor')) {
|
||||
define("LOAD_MODE", 1); // composer项目模式
|
||||
define("LOAD_MODE_COMPOSER_PATH", getcwd());
|
||||
} else {
|
||||
define("LOAD_MODE", 0); // 源码模式
|
||||
}
|
||||
|
||||
//if (LOAD_MODE === 0) $this->add(new BuildCommand()); //只有在git源码模式才能使用打包指令
|
||||
if (LOAD_MODE === 0) define("WORKING_DIR", getcwd());
|
||||
elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../"));
|
||||
elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL;
|
||||
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
|
||||
}
|
||||
if (LOAD_MODE == 2) {
|
||||
// Phar 模式,2.0 不提供哦
|
||||
//require_once FRAMEWORK_DIR . "/vendor/autoload.php";
|
||||
spl_autoload_register('phar_classloader');
|
||||
} elseif (LOAD_MODE == 0) {
|
||||
/** @noinspection PhpIncludeInspection
|
||||
* @noinspection RedundantSuppression
|
||||
*/
|
||||
require_once WORKING_DIR . "/vendor/autoload.php";
|
||||
if (LOAD_MODE == 0) {
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
|
||||
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
|
||||
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载,是否添加?[Y/n] ";
|
||||
@@ -97,14 +95,9 @@ class ConsoleApplication extends Application
|
||||
}
|
||||
|
||||
private function selfCheck() {
|
||||
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19");
|
||||
if (!extension_loaded("swoole")) die("Can not find swoole extension.\nSee: https://github.com/zhamao-robot/zhamao-framework/issues/19\n");
|
||||
if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
|
||||
//if (!extension_loaded("gd")) die("Can not find gd extension.\n");
|
||||
//if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
|
||||
if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n");
|
||||
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
|
||||
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
|
||||
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
|
||||
if (version_compare(PHP_VERSION, "7.2") == -1) die("PHP >= 7.2 required.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use swoole_server;
|
||||
use ZM\ConnectionManager\ConnectionObject;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Exception\WaitTimeoutException;
|
||||
use ZM\Http\Response;
|
||||
@@ -105,28 +106,35 @@ class Context implements ContextInterface
|
||||
* @return mixed
|
||||
*/
|
||||
public function reply($msg, $yield = false) {
|
||||
switch ($this->getData()["message_type"]) {
|
||||
case "group":
|
||||
case "private":
|
||||
case "discuss":
|
||||
$this->setCache("has_reply", true);
|
||||
$data = $this->getData();
|
||||
$conn = $this->getConnection();
|
||||
switch ($data["message_type"]) {
|
||||
case "group":
|
||||
return (new ZMRobot($conn))->setCallback($yield)->sendGroupMsg($data["group_id"], $msg);
|
||||
case "private":
|
||||
return (new ZMRobot($conn))->setCallback($yield)->sendPrivateMsg($data["user_id"], $msg);
|
||||
}
|
||||
return null;
|
||||
$data = $this->getData();
|
||||
$conn = $this->getConnection();
|
||||
if (!is_array($msg)) {
|
||||
switch ($this->getData()["message_type"]) {
|
||||
case "group":
|
||||
case "private":
|
||||
case "discuss":
|
||||
$this->setCache("has_reply", true);
|
||||
$operation["reply"] = $msg;
|
||||
$operation["at_sender"] = false;
|
||||
return (new ZMRobot($conn))->setCallback($yield)->callExtendedAPI(".handle_quick_operation", [
|
||||
"context" => $data,
|
||||
"operation" => $operation
|
||||
]);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
$operation = $msg;
|
||||
return (new ZMRobot($conn))->setCallback(false)->callExtendedAPI(".handle_quick_operation", [
|
||||
"context" => $data,
|
||||
"operation" => $operation
|
||||
]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function finalReply($msg, $yield = false) {
|
||||
self::$context[$this->cid]["cache"]["block_continue"] = true;
|
||||
if ($msg == "") return true;
|
||||
return $this->reply($msg, $yield);
|
||||
if ($msg != "") $this->reply($msg, $yield);
|
||||
EventDispatcher::interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,7 +38,7 @@ class DB
|
||||
if (Table::getTableInstance($table_name) === null) {
|
||||
if (in_array($table_name, self::$table_list))
|
||||
return new Table($table_name);
|
||||
elseif(SqlPoolStorage::$sql_pool !== null){
|
||||
elseif (SqlPoolStorage::$sql_pool !== null) {
|
||||
throw new DbException("Table " . $table_name . " not exist in database.");
|
||||
} else {
|
||||
throw new DbException("Database connection not exist or connect failed. Please check sql configuration");
|
||||
@@ -84,7 +84,8 @@ class DB
|
||||
* @throws DbException
|
||||
*/
|
||||
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
|
||||
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
|
||||
if (!is_array($params)) $params = [$params];
|
||||
Console::debug("MySQL: " . $line . " | " . implode(", ", $params));
|
||||
try {
|
||||
$conn = SqlPoolStorage::$sql_pool->get();
|
||||
if ($conn === false) {
|
||||
@@ -115,7 +116,7 @@ class DB
|
||||
return $ps->fetchAll($fetch_mode);
|
||||
}
|
||||
} catch (DbException $e) {
|
||||
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
zm_sleep(0.2);
|
||||
Console::warning("Gone away of MySQL! retrying!");
|
||||
return self::rawQuery($line, $params);
|
||||
@@ -123,7 +124,7 @@ class DB
|
||||
Console::warning($e->getMessage());
|
||||
throw $e;
|
||||
} catch (PDOException $e) {
|
||||
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
if (mb_strpos($e->getMessage(), "has gone away") !== false) {
|
||||
zm_sleep(0.2);
|
||||
Console::warning("Gone away of MySQL! retrying!");
|
||||
return self::rawQuery($line, $params);
|
||||
|
||||
@@ -28,6 +28,6 @@ class InsertBody
|
||||
* @throws DbException
|
||||
*/
|
||||
public function save() {
|
||||
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES ('.implode(',', array_fill(0, count($this->row), '?')).')', $this->row);
|
||||
DB::rawQuery('INSERT INTO ' . $this->table->getTableName() . ' VALUES (' . implode(',', array_fill(0, count($this->row), '?')) . ')', $this->row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
namespace ZM\DB;
|
||||
|
||||
|
||||
|
||||
class Table
|
||||
{
|
||||
private $table_name;
|
||||
@@ -28,7 +27,7 @@ class Table
|
||||
return new SelectBody($this, $what == [] ? ["*"] : $what);
|
||||
}
|
||||
|
||||
public function where($column, $operation_or_value, $value = null){
|
||||
public function where($column, $operation_or_value, $value = null) {
|
||||
return (new SelectBody($this, ["*"]))->where($column, $operation_or_value, $value);
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ class Table
|
||||
return new DeleteBody($this);
|
||||
}
|
||||
|
||||
public function statement(){
|
||||
public function statement() {
|
||||
$this->cache = [];
|
||||
//TODO: 无返回的statement语句
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class UpdateBody
|
||||
* @var array
|
||||
*/
|
||||
private $set_value;
|
||||
|
||||
/**
|
||||
* UpdateBody constructor.
|
||||
* @param Table $table
|
||||
@@ -31,19 +32,19 @@ class UpdateBody
|
||||
/**
|
||||
* @throws DbException
|
||||
*/
|
||||
public function save(){
|
||||
public function save() {
|
||||
$arr = [];
|
||||
$msg = [];
|
||||
foreach($this->set_value as $k => $v) {
|
||||
$msg []= $k .' = ?';
|
||||
$arr[]=$v;
|
||||
foreach ($this->set_value as $k => $v) {
|
||||
$msg [] = $k . ' = ?';
|
||||
$arr[] = $v;
|
||||
}
|
||||
if(($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
|
||||
$line = 'UPDATE '.$this->table->getTableName().' SET '.implode(', ', $msg);
|
||||
if($this->where_thing != []) {
|
||||
if (($msg ?? []) == []) throw new DbException('update value sets can not be empty!');
|
||||
$line = 'UPDATE ' . $this->table->getTableName() . ' SET ' . implode(', ', $msg);
|
||||
if ($this->where_thing != []) {
|
||||
list($sql, $param) = $this->getWhereSQL();
|
||||
$arr = array_merge($arr, $param);
|
||||
$line .= ' WHERE '.$sql;
|
||||
$line .= ' WHERE ' . $sql;
|
||||
}
|
||||
return DB::rawQuery($line, $arr);
|
||||
}
|
||||
|
||||
@@ -15,17 +15,17 @@ trait WhereBody
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getWhereSQL(){
|
||||
protected function getWhereSQL() {
|
||||
$param = [];
|
||||
$msg = '';
|
||||
foreach($this->where_thing as $k => $v) {
|
||||
foreach($v as $ks => $vs) {
|
||||
if($param != []) {
|
||||
$msg .= ' AND '.$ks ." $k ?";
|
||||
foreach ($this->where_thing as $k => $v) {
|
||||
foreach ($v as $ks => $vs) {
|
||||
if ($param != []) {
|
||||
$msg .= ' AND ' . $ks . " $k ?";
|
||||
} else {
|
||||
$msg .= "$ks $k ?";
|
||||
}
|
||||
$param []=$vs;
|
||||
$param [] = $vs;
|
||||
}
|
||||
}
|
||||
if ($msg == '') $msg = 1;
|
||||
|
||||
@@ -93,13 +93,13 @@ class EventDispatcher
|
||||
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
|
||||
$this->dispatchEvent($v, $this->rule, ...$params);
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
|
||||
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
|
||||
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
|
||||
if (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
|
||||
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
|
||||
($this->return_func)($this->store);
|
||||
}
|
||||
}
|
||||
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
|
||||
if ($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
|
||||
} catch (InterruptException $e) {
|
||||
$this->store = $e->return_var;
|
||||
$this->status = self::STATUS_INTERRUPTED;
|
||||
@@ -113,9 +113,9 @@ class EventDispatcher
|
||||
* @param mixed $v
|
||||
* @param null $rule_func
|
||||
* @param mixed ...$params
|
||||
* @throws AnnotationException
|
||||
* @throws InterruptException
|
||||
* @return bool
|
||||
* @throws InterruptException
|
||||
* @throws AnnotationException
|
||||
*/
|
||||
public function dispatchEvent($v, $rule_func = null, ...$params) {
|
||||
$q_c = $v->class;
|
||||
|
||||
@@ -9,8 +9,11 @@ use Exception;
|
||||
use Swoole\Timer;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Swoole\OnSave;
|
||||
use ZM\Annotation\Swoole\OnTick;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\ZMAtomic;
|
||||
|
||||
class EventManager
|
||||
@@ -59,5 +62,11 @@ class EventManager
|
||||
}
|
||||
});
|
||||
}
|
||||
$conf = ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if (server()->worker_id == $conf["worker"]) {
|
||||
zm_timer_tick(ZMConfig::get("global", "light_cache")["auto_save_interval"] * 1000, function () {
|
||||
LightCache::savePersistence();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace ZM\Event;
|
||||
|
||||
|
||||
use Closure;
|
||||
use Co;
|
||||
use Error;
|
||||
use Exception;
|
||||
@@ -41,6 +42,7 @@ use ZM\Exception\InterruptException;
|
||||
use ZM\Framework;
|
||||
use ZM\Http\Response;
|
||||
use ZM\Module\QQBot;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\MySQL\SqlPoolStorage;
|
||||
use ZM\Store\Redis\ZMRedisPool;
|
||||
@@ -78,11 +80,16 @@ class ServerEventHandler
|
||||
});
|
||||
}
|
||||
Process::signal(SIGINT, function () use ($r) {
|
||||
echo "\r";
|
||||
Console::warning("Server interrupted(SIGINT) on Master.");
|
||||
if ((Framework::$server->inotify ?? null) !== null)
|
||||
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
|
||||
ZMUtil::stop();
|
||||
if (zm_atomic("_int_is_reload")->get() === 1) {
|
||||
zm_atomic("_int_is_reload")->set(0);
|
||||
ZMUtil::reload();
|
||||
} else {
|
||||
echo "\r";
|
||||
Console::warning("Server interrupted(SIGINT) on Master.");
|
||||
if ((Framework::$server->inotify ?? null) !== null)
|
||||
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
|
||||
ZMUtil::stop();
|
||||
}
|
||||
});
|
||||
if (Framework::$argv["daemon"]) {
|
||||
$daemon_data = json_encode([
|
||||
@@ -121,6 +128,9 @@ class ServerEventHandler
|
||||
* @param $worker_id
|
||||
*/
|
||||
public function onWorkerStop(Server $server, $worker_id) {
|
||||
if ($worker_id == (ZMConfig::get("worker_cache")["worker"] ?? 0)) {
|
||||
LightCache::savePersistence();
|
||||
}
|
||||
Console::debug(($server->taskworker ? "Task" : "") . "Worker #$worker_id 已停止");
|
||||
}
|
||||
|
||||
@@ -406,9 +416,16 @@ class ServerEventHandler
|
||||
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
|
||||
unset(Context::$context[Co::getCid()]);
|
||||
$type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? "");
|
||||
$access_token = explode(" ", $request->header["authorization"] ?? $request->get["token"] ?? "")[1] ?? "";
|
||||
if (($a = ZMConfig::get("global", "access_token")) != "") {
|
||||
if ($access_token !== $a) {
|
||||
$access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? "";
|
||||
$token = ZMConfig::get("global", "access_token");
|
||||
if ($token instanceof Closure) {
|
||||
if (!$token($access_token)) {
|
||||
$server->close($request->fd);
|
||||
Console::warning("Unauthorized access_token: " . $access_token);
|
||||
return;
|
||||
}
|
||||
} elseif (is_string($token)) {
|
||||
if ($access_token !== $token && $token !== "") {
|
||||
$server->close($request->fd);
|
||||
Console::warning("Unauthorized access_token: " . $access_token);
|
||||
return;
|
||||
@@ -533,6 +550,11 @@ class ServerEventHandler
|
||||
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
|
||||
$server->sendMessage(json_encode($action, 256), $src_worker_id);
|
||||
break;
|
||||
case "hasKeyWorkerCache":
|
||||
$r = WorkerCache::hasKey($data["key"], $data["subkey"]);
|
||||
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
|
||||
$server->sendMessage(json_encode($action, 256), $src_worker_id);
|
||||
break;
|
||||
case "asyncAddWorkerCache":
|
||||
WorkerCache::add($data["key"], $data["value"], true);
|
||||
break;
|
||||
|
||||
@@ -9,6 +9,7 @@ use Exception;
|
||||
use ZM\Annotation\Swoole\OnSetup;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\ConnectionManager\ManagerGM;
|
||||
use ZM\Console\TermColor;
|
||||
use ZM\Event\ServerEventHandler;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\LightCacheInside;
|
||||
@@ -73,8 +74,7 @@ class Framework
|
||||
die($e->getMessage());
|
||||
}
|
||||
try {
|
||||
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
|
||||
$this->server_set = ZMConfig::get("global", "swoole");
|
||||
|
||||
Console::init(
|
||||
ZMConfig::get("global", "info_level") ?? 2,
|
||||
self::$server,
|
||||
@@ -85,37 +85,41 @@ class Framework
|
||||
$timezone = ZMConfig::get("global", "timezone") ?? "Asia/Shanghai";
|
||||
date_default_timezone_set($timezone);
|
||||
|
||||
$this->server_set = ZMConfig::get("global", "swoole");
|
||||
$this->parseCliArgs(self::$argv);
|
||||
|
||||
$out = [
|
||||
"host" => ZMConfig::get("global", "host"),
|
||||
"port" => ZMConfig::get("global", "port"),
|
||||
"log_level" => Console::getLevel(),
|
||||
"version" => ZM_VERSION,
|
||||
"config" => $args["env"] === null ? 'global.php' : $args["env"]
|
||||
];
|
||||
if(APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
|
||||
// 打印初始信息
|
||||
$out["listen"] = ZMConfig::get("global", "host") . ":" . ZMConfig::get("global", "port");
|
||||
if (!isset(ZMConfig::get("global", "swoole")["worker_num"])) $out["worker"] = swoole_cpu_num() . " (auto)";
|
||||
else $out["worker"] = ZMConfig::get("global", "swoole")["worker_num"];
|
||||
$out["environment"] = $args["env"] === null ? "default" : $args["env"];
|
||||
$out["log_level"] = Console::getLevel();
|
||||
$out["version"] = ZM_VERSION;
|
||||
if (APP_VERSION !== "unknown") $out["app_version"] = APP_VERSION;
|
||||
if (isset(ZMConfig::get("global", "swoole")["task_worker_num"])) {
|
||||
$out["task_worker_num"] = ZMConfig::get("global", "swoole")["task_worker_num"];
|
||||
$out["task_worker"] = ZMConfig::get("global", "swoole")["task_worker_num"];
|
||||
}
|
||||
if (($num = ZMConfig::get("global", "swoole")["worker_num"] ?? swoole_cpu_num()) != 1) {
|
||||
$out["worker_num"] = $num;
|
||||
if (ZMConfig::get("global", "sql_config")["sql_host"] !== "") {
|
||||
$conf = ZMConfig::get("global", "sql_config");
|
||||
$out["mysql_pool"] = $conf["sql_database"] . "@" . $conf["sql_host"] . ":" . $conf["sql_port"];
|
||||
}
|
||||
if (ZMConfig::get("global", "redis_config")["host"] !== "") {
|
||||
$conf = ZMConfig::get("global", "redis_config");
|
||||
$out["redis_pool"] = $conf["host"] . ":" . $conf["port"];
|
||||
}
|
||||
if (ZMConfig::get("global", "static_file_server")["status"] !== false) {
|
||||
$out["static_file_server"] = "enabled";
|
||||
}
|
||||
|
||||
$out["working_dir"] = DataProvider::getWorkingDir();
|
||||
Console::printProps($out, $tty_width);
|
||||
self::printProps($out, $tty_width, $args["log-theme"] === null);
|
||||
|
||||
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
|
||||
|
||||
self::$server->set($this->server_set);
|
||||
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
|
||||
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
|
||||
} else {
|
||||
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
|
||||
}
|
||||
$motd = explode("\n", $motd);
|
||||
foreach ($motd as $k => $v) {
|
||||
$motd[$k] = substr($v, 0, $tty_width);
|
||||
}
|
||||
$motd = implode("\n", $motd);
|
||||
echo $motd;
|
||||
|
||||
self::printMotd($tty_width);
|
||||
|
||||
global $asd;
|
||||
$asd = get_included_files();
|
||||
// 注册 Swoole Server 的事件
|
||||
@@ -167,10 +171,25 @@ class Framework
|
||||
} catch (Exception $e) {
|
||||
Console::error("Framework初始化出现错误,请检查!");
|
||||
Console::error($e->getMessage());
|
||||
Console::debug($e);
|
||||
die;
|
||||
}
|
||||
}
|
||||
|
||||
private static function printMotd($tty_width) {
|
||||
if (file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
|
||||
$motd = file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
|
||||
} else {
|
||||
$motd = file_get_contents(__DIR__ . "/../../config/motd.txt");
|
||||
}
|
||||
$motd = explode("\n", $motd);
|
||||
foreach ($motd as $k => $v) {
|
||||
$motd[$k] = substr($v, 0, $tty_width);
|
||||
}
|
||||
$motd = implode("\n", $motd);
|
||||
echo $motd;
|
||||
}
|
||||
|
||||
public function start() {
|
||||
self::$server->start();
|
||||
}
|
||||
@@ -223,16 +242,7 @@ class Framework
|
||||
private function parseCliArgs($args) {
|
||||
$coroutine_mode = true;
|
||||
global $terminal_id;
|
||||
$terminal_id = call_user_func(function () {
|
||||
try {
|
||||
$data = random_bytes(16);
|
||||
} catch (Exception $e) {
|
||||
return "";
|
||||
}
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
|
||||
});
|
||||
$terminal_id = uuidgen();
|
||||
foreach ($args as $x => $y) {
|
||||
switch ($x) {
|
||||
case 'disable-coroutine':
|
||||
@@ -289,6 +299,92 @@ class Framework
|
||||
if ($coroutine_mode) Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
|
||||
}
|
||||
|
||||
private static function writeNoDouble($k, $v, &$line_data, &$line_width, &$current_line, $colorful, $max_border) {
|
||||
$tmp_line = $k . ": " . $v;
|
||||
//Console::info("写入[".$tmp_line."]");
|
||||
if (strlen($tmp_line) >= $line_width[$current_line]) { //输出的内容太多了,以至于一行都放不下一个,要折行
|
||||
$title_strlen = strlen($k . ": ");
|
||||
$content_len = $line_width[$current_line] - $title_strlen;
|
||||
|
||||
$line_data[$current_line] = " " . $k . ": ";
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
|
||||
$line_data[$current_line] .= substr($v, 0, $content_len);
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
|
||||
$rest = substr($v, $content_len);
|
||||
++$current_line; // 带标题的第一行满了,折到第二行
|
||||
do {
|
||||
if ($colorful) $line_data[$current_line] = TermColor::color8(32);
|
||||
$line_data[$current_line] .= " " . substr($rest, 0, $max_border - 2);
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
|
||||
$rest = substr($rest, $max_border - 2);
|
||||
++$current_line;
|
||||
} while ($rest > $max_border - 2); // 循环,直到放完
|
||||
} else { // 不需要折行
|
||||
//Console::info("不需要折行");
|
||||
$line_data[$current_line] = " " . $k . ": ";
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
|
||||
$line_data[$current_line] .= $v;
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
|
||||
|
||||
if ($max_border >= 57) {
|
||||
if (strlen($tmp_line) >= intval(($max_border - 2) / 2)) { // 不需要折行,直接输出一个转下一行
|
||||
//Console::info("不需要折行,直接输出一个转下一行");
|
||||
++$current_line;
|
||||
} else { // 输出很小,写到前面并分片
|
||||
//Console::info("输出很小,写到前面并分片");
|
||||
$space = intval($max_border / 2) - 2 - strlen($tmp_line);
|
||||
$line_data[$current_line] .= str_pad("", $space, " ");
|
||||
$line_data[$current_line] .= "| "; // 添加分片
|
||||
$line_width[$current_line] -= (strlen($tmp_line) + 3 + $space);
|
||||
}
|
||||
} else {
|
||||
++$current_line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function printProps($out, $tty_width, $colorful = true) {
|
||||
$max_border = $tty_width < 65 ? $tty_width : 65;
|
||||
if (LOAD_MODE == 0) echo Console::setColor("* Framework started with source mode.\n", $colorful ? "yellow" : "");
|
||||
echo str_pad("", $max_border, "=") . PHP_EOL;
|
||||
|
||||
$current_line = 0;
|
||||
$line_width = [];
|
||||
$line_data = [];
|
||||
foreach ($out as $k => $v) {
|
||||
start:
|
||||
if (!isset($line_width[$current_line])) {
|
||||
$line_width[$current_line] = $max_border - 2;
|
||||
}
|
||||
//Console::info("行宽[$current_line]:".$line_width[$current_line]);
|
||||
if ($max_border >= 57) { // 很宽的时候,一行能放两个短行
|
||||
if ($line_width[$current_line] == ($max_border - 2)) { //空行
|
||||
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
|
||||
} else { // 不是空行,已经有东西了
|
||||
$tmp_line = $k . ": " . $v;
|
||||
//Console::info("[$current_line]即将插入后面的东西[".$tmp_line."]");
|
||||
if (strlen($tmp_line) > $line_width[$current_line]) { // 地方不够,另起一行
|
||||
$line_data[$current_line] = str_replace("| ", "", $line_data[$current_line]);
|
||||
++$current_line;
|
||||
goto start;
|
||||
} else { // 地方够,直接写到后面并另起一行
|
||||
$line_data[$current_line] .= $k . ": ";
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::color8(32);
|
||||
$line_data[$current_line] .= $v;
|
||||
if ($colorful) $line_data[$current_line] .= TermColor::RESET;
|
||||
++$current_line;
|
||||
}
|
||||
}
|
||||
} else { // 不够宽,直接写单行
|
||||
self::writeNoDouble($k, $v, $line_data, $line_width, $current_line, $colorful, $max_border);
|
||||
}
|
||||
}
|
||||
foreach ($line_data as $v) {
|
||||
echo $v . PHP_EOL;
|
||||
}
|
||||
echo str_pad("", $max_border, "=") . PHP_EOL;
|
||||
}
|
||||
|
||||
public static function getTtyWidth() {
|
||||
return explode(" ", trim(exec("stty size")))[1];
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class Response
|
||||
* @return mixed
|
||||
*/
|
||||
public function end($content = null) {
|
||||
if(!$this->is_end) {
|
||||
if (!$this->is_end) {
|
||||
$this->is_end = true;
|
||||
return $this->response->end($content);
|
||||
} else {
|
||||
|
||||
@@ -16,7 +16,7 @@ class RouteManager
|
||||
public static $routes = null;
|
||||
|
||||
public static function importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations) {
|
||||
if(self::$routes === null) self::$routes = new RouteCollection();
|
||||
if (self::$routes === null) self::$routes = new RouteCollection();
|
||||
|
||||
// 拿到所属方法的类上面有没有控制器的注解
|
||||
$prefix = '';
|
||||
@@ -27,8 +27,8 @@ class RouteManager
|
||||
}
|
||||
}
|
||||
$tail = trim($vss->route, "/");
|
||||
$route_name = $prefix.($tail === "" ? "" : "/").$tail;
|
||||
Console::debug("添加路由:".$route_name);
|
||||
$route_name = $prefix . ($tail === "" ? "" : "/") . $tail;
|
||||
Console::debug("添加路由:" . $route_name);
|
||||
$route = new Route($route_name, ['_class' => $class, '_method' => $method]);
|
||||
$route->setMethods($vss->request_method);
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@ class StaticFileHandler
|
||||
public function __construct($filename, $path) {
|
||||
$full_path = realpath($path . "/" . $filename);
|
||||
$response = ctx()->getResponse();
|
||||
Console::debug("Full path: ".$full_path);
|
||||
Console::debug("Full path: " . $full_path);
|
||||
if ($full_path !== false) {
|
||||
if (strpos($full_path, $path) !== 0) {
|
||||
$response->status(403);
|
||||
$response->end("403 Forbidden");
|
||||
return true;
|
||||
} else {
|
||||
if(is_file($full_path)) {
|
||||
if (is_file($full_path)) {
|
||||
$exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown");
|
||||
$response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream");
|
||||
$response->end(file_get_contents($full_path));
|
||||
|
||||
@@ -6,7 +6,10 @@ namespace ZM\Store;
|
||||
|
||||
use Exception;
|
||||
use Swoole\Table;
|
||||
use ZM\Annotation\Swoole\OnSave;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Event\EventDispatcher;
|
||||
use ZM\Exception\ZMException;
|
||||
|
||||
class LightCache
|
||||
@@ -175,7 +178,16 @@ class LightCache
|
||||
return $r;
|
||||
}
|
||||
|
||||
public static function savePersistence() {
|
||||
public static function savePersistence($only_worker = false) {
|
||||
|
||||
// 下面将OnSave激活一下
|
||||
if (server()->worker_id == (ZMConfig::get("global", "worker_cache")["worker"] ?? 0)) {
|
||||
$dispatcher = new EventDispatcher(OnSave::class);
|
||||
$dispatcher->dispatchEvents();
|
||||
}
|
||||
|
||||
if($only_worker) return;
|
||||
|
||||
if (self::$kv_table === null) return;
|
||||
$r = [];
|
||||
foreach (self::$kv_table as $k => $v) {
|
||||
@@ -184,11 +196,13 @@ class LightCache
|
||||
$r[$k] = self::parseGet($v);
|
||||
}
|
||||
}
|
||||
if(self::$config["persistence_path"] == "") return;
|
||||
if (self::$config["persistence_path"] == "") return;
|
||||
if (file_exists(self::$config["persistence_path"])) {
|
||||
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
|
||||
if ($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static function checkExpire($key) {
|
||||
|
||||
@@ -15,18 +15,16 @@ class SpinLock
|
||||
|
||||
private static $delay = 1;
|
||||
|
||||
public static function init($key_cnt, $delay = 1)
|
||||
{
|
||||
public static function init($key_cnt, $delay = 1) {
|
||||
self::$kv_lock = new Table($key_cnt, 0.7);
|
||||
self::$delay = $delay;
|
||||
self::$kv_lock->column('lock_num', Table::TYPE_INT, 8);
|
||||
return self::$kv_lock->create();
|
||||
}
|
||||
|
||||
public static function lock(string $key)
|
||||
{
|
||||
public static function lock(string $key) {
|
||||
while (($r = self::$kv_lock->incr($key, 'lock_num')) > 1) { //此资源已经被锁上了
|
||||
if(Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
|
||||
if (Coroutine::getCid() != -1) System::sleep(self::$delay / 1000);
|
||||
else usleep(self::$delay * 1000);
|
||||
}
|
||||
}
|
||||
@@ -41,4 +39,10 @@ class SpinLock
|
||||
public static function unlock(string $key) {
|
||||
return self::$kv_lock->set($key, ['lock_num' => 0]);
|
||||
}
|
||||
|
||||
public static function transaction(string $key, callable $function) {
|
||||
SpinLock::lock($key);
|
||||
$function();
|
||||
SpinLock::unlock($key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ZMRedis
|
||||
* @throws NotInitializedException
|
||||
*/
|
||||
public static function call(callable $callable) {
|
||||
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
$r = ZMRedisPool::$pool->get();
|
||||
$result = $callable($r);
|
||||
if (isset($r->wasted)) ZMRedisPool::$pool->put(null);
|
||||
@@ -29,7 +29,7 @@ class ZMRedis
|
||||
* @throws NotInitializedException
|
||||
*/
|
||||
public function __construct() {
|
||||
if(ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
if (ZMRedisPool::$pool === null) throw new NotInitializedException("Redis pool is not initialized.");
|
||||
$this->conn = ZMRedisPool::$pool->get();
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ class ZMRedisPool
|
||||
);
|
||||
try {
|
||||
$r = self::$pool->get()->ping('123');
|
||||
if(strpos(strtolower($r), "123") !== false) {
|
||||
if (strpos(strtolower($r), "123") !== false) {
|
||||
Console::debug("成功连接redis连接池!");
|
||||
} else {
|
||||
var_dump($r);
|
||||
}
|
||||
} catch (RedisException $e) {
|
||||
Console::error("Redis init failed! ".$e->getMessage());
|
||||
Console::error("Redis init failed! " . $e->getMessage());
|
||||
self::$pool = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,20 @@ class WorkerCache
|
||||
return self::processRemote($action, $async, $config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function hasKey($key, $subkey) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
return isset(self::$store[$key][$subkey]);
|
||||
} else {
|
||||
$action = ["hasKeyWorkerCache", "key" => $key, "subkey" => $subkey, "cid" => zm_cid()];
|
||||
return self::processRemote($action, false, $config);
|
||||
}
|
||||
}
|
||||
|
||||
private static function processRemote($action, $async, $config) {
|
||||
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
|
||||
if(!$ss) return false;
|
||||
if (!$ss) return false;
|
||||
if ($async) return true;
|
||||
zm_yield();
|
||||
$p = self::$transfer[zm_cid()] ?? null;
|
||||
@@ -63,7 +73,7 @@ class WorkerCache
|
||||
public static function add($key, int $value, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
if(!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
if (!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
self::$store[$key] += $value;
|
||||
return true;
|
||||
} else {
|
||||
@@ -75,7 +85,7 @@ class WorkerCache
|
||||
public static function sub($key, int $value, $async = false) {
|
||||
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
|
||||
if ($config["worker"] === server()->worker_id) {
|
||||
if(!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
if (!isset(self::$store[$key])) self::$store[$key] = 0;
|
||||
self::$store[$key] -= $value;
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -28,6 +28,7 @@ class ZMAtomic
|
||||
self::$atomics[$k] = new Atomic($v);
|
||||
}
|
||||
self::$atomics["stop_signal"] = new Atomic(0);
|
||||
self::$atomics["_int_is_reload"] = new Atomic(0);
|
||||
self::$atomics["wait_msg_id"] = new Atomic(0);
|
||||
self::$atomics["_event_id"] = new Atomic(0);
|
||||
for ($i = 0; $i < 10; ++$i) {
|
||||
|
||||
@@ -55,7 +55,7 @@ class CoMessage
|
||||
SpinLock::lock("wait_api");
|
||||
$all = LightCacheInside::get("wait_api", "wait_api") ?? [];
|
||||
foreach ($all as $k => $v) {
|
||||
if(!isset($v["compare"])) continue;
|
||||
if (!isset($v["compare"])) continue;
|
||||
foreach ($v["compare"] as $vs) {
|
||||
if (!isset($v[$vs], $dat[$vs])) continue 2;
|
||||
if ($v[$vs] != $dat[$vs]) {
|
||||
@@ -64,7 +64,7 @@ class CoMessage
|
||||
}
|
||||
$last = $k;
|
||||
}
|
||||
if($last !== null) {
|
||||
if ($last !== null) {
|
||||
$all[$last]["result"] = $dat;
|
||||
LightCacheInside::set("wait_api", "wait_api", $all);
|
||||
SpinLock::unlock("wait_api");
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace ZM\Utils;
|
||||
|
||||
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Console\Console;
|
||||
|
||||
class DataProvider
|
||||
{
|
||||
@@ -15,7 +16,7 @@ class DataProvider
|
||||
}
|
||||
|
||||
public static function getWorkingDir() {
|
||||
if(LOAD_MODE == 0) return WORKING_DIR;
|
||||
if (LOAD_MODE == 0) return WORKING_DIR;
|
||||
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
|
||||
elseif (LOAD_MODE == 2) return realpath('.');
|
||||
return null;
|
||||
@@ -28,4 +29,29 @@ class DataProvider
|
||||
public static function getDataFolder() {
|
||||
return ZM_DATA;
|
||||
}
|
||||
|
||||
public static function saveToJson($filename, $file_array) {
|
||||
$path = ZMConfig::get("global", "config_dir");
|
||||
$r = explode("/", $filename);
|
||||
if(count($r) == 2) {
|
||||
$path = $path . $r[0]."/";
|
||||
if(!is_dir($path)) mkdir($path);
|
||||
$name = $r[1];
|
||||
} elseif (count($r) != 1) {
|
||||
Console::warning("存储失败,文件名只能有一级目录");
|
||||
return false;
|
||||
} else {
|
||||
$name = $r[0];
|
||||
}
|
||||
return file_put_contents($path.$name.".json", json_encode($file_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
public static function loadFromJson($filename) {
|
||||
$path = ZMConfig::get("global", "config_dir");
|
||||
if(file_exists($path.$filename.".json")) {
|
||||
return json_decode(file_get_contents($path.$filename.".json"), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,16 @@ use Swoole\Timer;
|
||||
use ZM\Console\Console;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\LightCacheInside;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
use ZM\Store\ZMAtomic;
|
||||
use ZM\Store\ZMBuf;
|
||||
|
||||
class ZMUtil
|
||||
{
|
||||
public static function stop() {
|
||||
if (SpinLock::tryLock("_stop_signal") === false) return;
|
||||
Console::warning(Console::setColor("Stopping server...", "red"));
|
||||
Console::trace();
|
||||
LightCache::savePersistence();
|
||||
if (ZMBuf::$terminal !== null)
|
||||
Event::del(ZMBuf::$terminal);
|
||||
@@ -31,6 +34,12 @@ class ZMUtil
|
||||
}
|
||||
|
||||
public static function reload($delay = 800) {
|
||||
if (server()->worker_id !== -1) {
|
||||
Console::info(server()->worker_id);
|
||||
zm_atomic("_int_is_reload")->set(1);
|
||||
system("kill -INT " . intval(server()->master_pid));
|
||||
return;
|
||||
}
|
||||
Console::info(Console::setColor("Reloading server...", "gold"));
|
||||
usleep($delay * 1000);
|
||||
foreach ((LightCacheInside::get("wait_api", "wait_api") ?? []) as $k => $v) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use ZM\Utils\DataProvider;
|
||||
define("ZM_START_TIME", microtime(true));
|
||||
define("ZM_DATA", ZMConfig::get("global", "zm_data"));
|
||||
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
|
||||
define("APP_VERSION", json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown");
|
||||
define("APP_VERSION", LOAD_MODE == 1 ? (json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true)["version"] ?? "unknown") : "unknown");
|
||||
define("CRASH_DIR", ZMConfig::get("global", "crash_dir"));
|
||||
@mkdir(ZM_DATA);
|
||||
@mkdir(CRASH_DIR);
|
||||
|
||||
@@ -89,10 +89,10 @@ function getAllClasses($dir, $indoor_name) {
|
||||
//echo "At " . $indoor_name . PHP_EOL;
|
||||
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
|
||||
elseif (mb_substr($v, -4) == ".php") {
|
||||
if(substr(file_get_contents($dir.$v), 6, 6) == "#plain") continue;
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir()."/composer.json"), true);
|
||||
foreach($composer["autoload"]["files"] as $fi) {
|
||||
if(realpath(DataProvider::getWorkingDir()."/".$fi) == realpath($dir.$v)) {
|
||||
if (substr(file_get_contents($dir . $v), 6, 6) == "#plain") continue;
|
||||
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
|
||||
foreach ($composer["autoload"]["files"] as $fi) {
|
||||
if (realpath(DataProvider::getWorkingDir() . "/" . $fi) == realpath($dir . $v)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
@@ -267,10 +267,14 @@ function zm_timer_after($ms, callable $callable) {
|
||||
}
|
||||
|
||||
function zm_timer_tick($ms, callable $callable) {
|
||||
go(function () use ($ms, $callable) {
|
||||
Console::debug("Adding extra timer tick of " . $ms . " ms");
|
||||
Swoole\Timer::tick($ms, $callable);
|
||||
});
|
||||
if (zm_cid() === -1) {
|
||||
return go(function () use ($ms, $callable) {
|
||||
Console::debug("Adding extra timer tick of " . $ms . " ms");
|
||||
Swoole\Timer::tick($ms, $callable);
|
||||
});
|
||||
} else {
|
||||
return Swoole\Timer::tick($ms, $callable);
|
||||
}
|
||||
}
|
||||
|
||||
function zm_data_hash($v) {
|
||||
@@ -313,4 +317,23 @@ function getAllFdByConnectType(string $type = 'default'): array {
|
||||
|
||||
function zm_atomic($name) {
|
||||
return \ZM\Store\ZMAtomic::get($name);
|
||||
}
|
||||
}
|
||||
|
||||
function uuidgen($uppercase = false) {
|
||||
try {
|
||||
$data = random_bytes(16);
|
||||
} catch (Exception $e) {
|
||||
return "";
|
||||
}
|
||||
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||
return $uppercase ? strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4))) :
|
||||
vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||
}
|
||||
|
||||
function working_dir() {
|
||||
if (LOAD_MODE == 0) return WORKING_DIR;
|
||||
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
|
||||
elseif (LOAD_MODE == 2) return realpath('.');
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user