Compare commits

...

60 Commits
2.2.0 ... 2.2.9

Author SHA1 Message Date
jerry
a23f3d8f16 update to 2.2.9 version
update reply() to support quick operation
fix reload bug
fix reply() bug
2021-03-06 17:22:42 +08:00
jerry
0c24bfdedd fix a motd bug 2021-03-02 21:31:06 +08:00
jerry
c0b95c6840 delete workflows 2021-03-02 21:27:04 +08:00
jerry
e977b09e20 Merge remote-tracking branch 'origin/master' 2021-03-02 21:24:53 +08:00
jerry
4ff75cf199 update to 2.2.8 version
update motd message
2021-03-02 21:24:31 +08:00
Whale
24e70c70ce Update deploy-docs.yml 2021-03-02 14:27:55 +08:00
Whale
275a7bf00b Update deploy-docs.yml 2021-03-02 14:26:02 +08:00
Whale
455fc79818 Update deploy-docs.yml 2021-03-02 14:22:40 +08:00
Whale
8740c3c255 Update deploy-docs.yml 2021-03-02 14:19:51 +08:00
Whale
98bfca5bb9 Update deploy-docs.yml 2021-03-02 14:18:40 +08:00
Whale
fc8d01ad5f Update deploy-docs.yml 2021-03-02 13:53:12 +08:00
Whale
d9b8df1725 Update deploy-docs.yml 2021-03-02 13:50:52 +08:00
Whale
9b7802ac04 Update deploy-docs.yml 2021-03-02 13:50:39 +08:00
Whale
6e1f3e0406 Update deploy-docs.yml 2021-03-02 13:43:45 +08:00
Whale
a2d4bab062 Update index.md 2021-03-02 13:40:21 +08:00
Whale
f1cefad910 Create deploy-docs.yml 2021-03-02 13:37:07 +08:00
jerry
957c69bd1e update to 2.2.7 version
fix reply() bug
fix access_token bug
2021-02-27 16:19:18 +08:00
Whale
2902c5e805 Merge pull request #33 from YiwanGi/master
Update ServerEventHandler.php
2021-02-27 16:13:46 +08:00
Wang
faf9f5d988 Update ServerEventHandler.php
-When the token is incorrect, repeated connection events occur in the framework
2021-02-27 00:04:02 +08:00
Whale
874f061468 Update README.md 2021-02-26 11:05:04 +08:00
jerry
69521a1f1f cleanup code, update some features
add Hitokoto API
add Closure for access_token
add working_dir() global function
adjust reply() method to .handle_quick_operation
2021-02-24 23:37:00 +08:00
Whale
fb9dbed306 Merge pull request #32 from wen1014/master
warning bug fix
2021-02-23 23:24:45 +08:00
Whale
d42158ac90 Merge branch 'master' into master 2021-02-23 23:24:24 +08:00
Whale
ff3ebec562 Merge pull request #31 from YiwanGi/patch-8
Update spin-lock.md
2021-02-23 23:22:01 +08:00
wenhao
ea79de617e warning bug fix 2021-02-23 17:04:10 +08:00
Wang
9e1ad6a983 Update spin-lock.md
-Forgotten data content
2021-02-22 19:34:06 +08:00
Whale
c17248df31 Merge pull request #30 from YiwanGi/patch-7
Update light-cache.md
2021-02-22 11:33:52 +08:00
Whale
4c116ebd86 Merge pull request #29 from YiwanGi/patch-5
Update console.md
2021-02-22 11:33:29 +08:00
Whale
c490fe0c1c Merge pull request #28 from YiwanGi/patch-6
Update route-annotations.md
2021-02-22 11:32:46 +08:00
Whale
cefdf23799 Merge pull request #27 from YiwanGi/patch-4
Update README.md
2021-02-22 11:32:06 +08:00
Wang
7f70642606 Update light-cache.md
-Follow up the latest configuration data
2021-02-22 02:51:35 +08:00
Wang
1d5b2609f9 Update console.md 2021-02-22 02:03:22 +08:00
Wang
a206fe8b87 Update route-annotations.md
-Correction of typos
2021-02-22 01:18:12 +08:00
Wang
fb4f6c45ce Update README.md
-Detail optimization
2021-02-22 01:07:35 +08:00
jerry
c50ae245bd commitment, nothing 2021-02-21 22:17:34 +08:00
Whale
f6c2131ebf Merge pull request #26 from YiwanGi/patch-3
Update README.md
2021-02-21 22:15:39 +08:00
Whale
543d1d2922 Merge pull request #25 from YiwanGi/patch-2
Update basic-config.md
2021-02-21 22:14:27 +08:00
Whale
bb61e6f6a2 Merge pull request #24 from YiwanGi/patch-1
Update quickstart-robot.md
2021-02-21 22:13:06 +08:00
YiwanGi
2d1bbf6b48 Update README.md
-Adjust the display format appropriately
-Solve the problem of no access to images in China
2021-02-21 12:48:54 +08:00
YiwanGi
67e42cfe3e Update basic-config.md
-Better display
2021-02-21 11:28:01 +08:00
YiwanGi
429a2cf230 Update quickstart-robot.md
-Better display
2021-02-21 10:48:23 +08:00
jerry
9ace85e604 update to 2.2.5 version again
add transaction for SpinLock.php
add getAllCQ() for CQ.php
fix CQ bug
update docs
2021-02-20 16:57:19 +08:00
jerry
f677b0e132 update to 2.2.5 version
add saveToJson and loadFromJson function for DataProvider.php
fix @OnSave annotation not working
adjust swoole timer tick
add hasKey() for WorkerCache.php
2021-02-15 15:15:26 +08:00
jerry
f137f044d0 Merge remote-tracking branch 'origin/master' 2021-02-09 17:09:26 +08:00
jerry
77c12db31a reformat code 2021-02-09 17:09:09 +08:00
Whale
b670cb29fe Update README.md 2021-02-09 11:12:29 +08:00
Whale
95d7bb071d Update README.md 2021-02-09 10:54:59 +08:00
Whale
eadb4c1dee Update README.md 2021-02-09 10:54:05 +08:00
Whale
6672a6c852 Update README.md 2021-02-09 10:53:52 +08:00
Whale
094feddda4 Update README.md 2021-02-09 10:53:15 +08:00
Whale
f86eddb298 Update README.md 2021-02-09 10:48:05 +08:00
Whale
a93b4917cd Update README.md 2021-02-09 10:47:40 +08:00
jerry
0f9767aa16 update docs 2021-02-07 11:48:55 +08:00
jerry
0c9f246690 update to 2.2.4 version
update docs
fix broken ssh caused cpu overloading
fix WorkerCache bug when no global config
add global function zm_atomic
2021-02-07 11:46:42 +08:00
jerry
517d258d61 update to 2.2.3 version, I am tired
fix access_token not working
fix waitMessage() not working in v2.2.2
2021-01-30 00:06:42 +08:00
jerry
61e3818563 update to 2.2.2 version finally
clean redundant code
fix API reply in @OnTick for multi-process
fix loop error reporting
2021-01-29 23:34:34 +08:00
jerry
776ec98a3e fix waitMessage timeout bug 2021-01-29 22:32:29 +08:00
jerry
f3e844bb0a update to 2.2.2 version
fix QQBot error
clean code
2021-01-29 22:27:10 +08:00
jerry
a55cd4ed05 update docs 2021-01-29 21:37:02 +08:00
jerry
8a985620f9 update to 2.2.1 version
fix a compatibility bug
2021-01-29 21:36:14 +08:00
74 changed files with 1644 additions and 505 deletions

View File

@@ -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>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes)
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![Banner](https://img.shields.io/badge/OneBot-v11-success)](https://github.com/howmanybots/onebot)
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
[![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](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)
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
![支付宝二维码](https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
@@ -86,7 +85,7 @@ public function index() {
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 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 等网站。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

View File

@@ -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();

View File

@@ -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.0",
"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": "*",

View File

@@ -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,

View File

@@ -1,6 +1,6 @@
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/

View File

@@ -1 +1,3 @@
# FAQ
这里会写一些常见的疑难解答。

View 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>

View File

@@ -0,0 +1,70 @@
# 框架多进程
首先对于多进程概念,对于传统 PHP 程序员可能比较陌生,唯一接触到的地方可能就是 php-fpm 等一些方式处理时间长的请求时开进程去执行。关于多进程,我觉得廖雪峰的 Python 多进程这段讲的不错:
> Unix/Linux 操作系统提供了一个`fork()`系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是`fork()`调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
这里面的重点在于,多进程的创建,是父进程的复制,然后两个进程接下来运行的代码和存的内容就分道扬镳了。
PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采用多进程呢?
## 作用
使用过框架的你一定知道,框架是以命令行方式运行 PHP 的,而命令行方式运行 PHP就代表要常驻内存就像 Python、Node.js 一样。而默认情况下,比如 Python 的 Flask 为单线程单进程模式,也就是说同时只能处理一个 Web 请求。但大部分情况下,比如 Node.js提供的都是异步 I/O这也就是说明它在 Web 处理请求上,可同时承接的 I/O 密集型请求会更多一些,这样在对一般的 Web 应用中 I/O 密集型场景非常有用,而且往往只需要单进程也可以承载上万的并发请求。
在炸毛框架中,因为框架基于 Swoole 构建,所以天然支持协程,而协程就是针对 I/O 操作进行一个调度,类似异步的 Node.js所以针对项目中存在太多的 SQL 语句执行、文件读写的话,炸毛框架直接上手,无需做任何修改,也可以达到很好的性能。
**但是**CPU 密集型的应用怎么办呢?假设我的 Web 应用有大量的排序、md5 运算怎么办呢?这样的阻塞,假设是一个超级大的 for 循环或者是要执行很长时间的 while 循环CPU 一直在被占用。多进程就是针对 CPU 密集型的应用说 yes 的一个方案。
![Untitled Diagram (1)](../assets/img/single-process.png)
我们假设现在有 3 个请求同时访问,也就是说上面的流程需要执行 3 遍。而如果我们只有一个进程的话,最后一个请求需要等待的时间为 `2*3+5*3=21` 秒,非常耗时。
而如果有两个进程处理 3 个请求,则最后一个完成的请求就缩短了,`2+5+2+5=14` 秒。
![Untitled Diagram (2)](../assets/img/Untitled Diagram (2).png)
所以如果要充分利用你的服务器或者个人电脑的多核 CPU 资源,就要设置多个进程来处理。一个进程只能在一个 CPU 上运行,而设置了多进程后,就可以让多核 CPU 充分运行多个进程,所以我们给框架设置多进程的推荐数值为等同于 CPU 的核心数。
## 为什么不是多线程
因为众所周知PHP 对线程的支持比较不好,而 ZTS 版本的 PHP 又会影响传统的 Web 端 PHP 的性能,再加上 Linux 对线程的切换效率和多进程切换的效率差不多,多线程容易造成数据读写不安全等问题,故 Swoole 使用的是多进程模型。
## 框架进程模型
![Untitled Diagram (3)](../assets/img/Untitled Diagram (3).png)
上图中,横向的时间片可以理解为并行执行,这些操作在多个 CPU 内可能同时在执行。
## 进程间隔离
众所周知,进程是程序在操作系统中的一个边界,和自己有关的一切变量、内容和代码都在自己的进程内,不同进程之间如果不使用管道等方式,是不可以互相访问的。而加上开始描述的,创建子进程是一个复制自身的过程,所以也就会有如下图的情况:
![Untitled Diagram (4)](../assets/img/Untitled Diagram (4).png)
我们以静态类为例,设置一个进程中的全局变量。这里就会出现,同一个静态变量在多个进程中完全不同的值的结果。此后,我们将会在 Worker 进程中执行用户的代码,如果设置 Worker 数量仅为 1 的话,那么就简单许多了,你还是可以使用全局变量或静态类来存储你想要的内容而不用担心这种多个进程变量隔离的情况(因为用户的 Web 请求处理的代码只会在一个 Worker 进程中执行)。如果像上图一样设置了多个 Worker则用户过来的比如 HTTP 请求就有可能出现在不同的 Worker 进程中,给全局变量设值就一定会造成不同步的问题。这时我们就不可以使用全局变量做数据同步(注意,我说的是数据同步)。
## 跨进程同步
跨进程同步方案中,框架给出了很多种解决方案。
- MySQL 数据库
- Redis
- LightCache 轻量缓存(共享内存)
- WorkerCache 大缓存
- ZMAtomic 跨进程原子计数器
下面的表格我将列出下方的特点和各自的优缺点:
| 类型 | 用途 | 优点 | 缺点 |
| ----------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
| MySQL | 大型的传统的关系式数据都可以用数据库,你懂的 | 就是数据库的优点 | 和数据库不在同一台服务器的话网络延迟会较大,数据获取效率不高 |
| Redis | 传统的 key-value 数据库 | 数据无同步等问题,性能高 | 有网络通信延迟 |
| LightCache | 框架封装的跨进程的 key-value 存储模型 | 性能强悍,无 I/O 和网络通信 | 需要提前分配最大内存大小,最大单个值长度大小,不灵活 |
| WorkerCache | 框架封装的基于进程的 key-value 存储模型,类似 Redis | 无需提前分配最大内存大小,受限于 PHP memory_limit | 见 WorkerCache 的说明 |
!!! note "WorkerCache 的说明"
对于 WorkerCache 来说其实是比较特殊的进程间通信。具体来说就是WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。

224
docs/assets/face_id.html Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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` 的相关章节。

View File

@@ -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 的颜色很浅,看不到,还有人使用不能显示颜色的黑白终端.....

View File

@@ -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>

View File

@@ -82,15 +82,20 @@ class Hello {
CQ 码字符反转义。
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `&#44;` 转义为 `,`
| 反转义前 | 反转义后 |
| -------- | -------- |
| `&amp;` | `&` |
| `&#91;` | `[` |
| `&#93;` | `]` |
| `&#44;` | `,` |
```php
$str = CQ::decode("&#91;我只是一条普通的文本&#93;");
// 转换为 "[我只是一条普通的文本]"
$str = CQ::decode("&#91;CQ:at,qq=我只是一条普通的文本&#93;");
// 转换为 "[CQ:at,qq=我只是一条普通的文本]"
```
### CQ::encode()
@@ -102,6 +107,14 @@ $str = CQ::encode("[CQ:我只是一条普通的文本]");
// $str: "&#91;CQ:我只是一条普通的文本&#93;"
```
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `,` 转义为 `&#44;`
### 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 表情
@@ -119,7 +174,7 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [炸毛框架 1.x 版本文档](https://docs-v1.zhamao.xin/face_list.html)。
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](/assets/face_id.html)。
```php
/**
@@ -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]
```

View 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()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。

View File

@@ -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 连接号。

View File

@@ -25,8 +25,8 @@
```php
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'size' => 512, //最多允许储存的条数需要2的倍数
'max_strlen' => 32768, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
@@ -292,7 +292,7 @@ class Hello {
* @CQCommand("set_store")
*/
public function setStorage() {
$arg1 = ctx()->getFullArg("请输入要设置的内容名称");
$arg1 = ctx()->getNextArg("请输入要设置的内容名称");
$arg2 = ctx()->getFullArg("请输入要设置的内容");
WorkerCache::set($arg1, $arg2);
return "成功!";

View File

@@ -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。

View File

@@ -246,7 +246,7 @@
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
class Hello {

View File

@@ -163,5 +163,5 @@ public function onThrowing(?Exception $e) {
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
`context()` 为获取当前协程空间绑定的 `request``response` 对象。
`ctx()` 为获取当前协程空间绑定的 `request``response` 对象。

View File

@@ -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 收到消息后触发的事件对应注解。

View File

@@ -4,7 +4,7 @@
!!! quote "开发提示"
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不嗦。
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不嗦。
## 控制器和路由
@@ -228,4 +228,4 @@ public function staticImage($param) {
}
```
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。

View File

@@ -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` 均可。

View File

@@ -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

View File

@@ -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 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。

View File

@@ -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 机器人聊天接口标准
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。

View File

@@ -42,7 +42,7 @@ function setCookie(name, value) {
var Days = 30;
var exp = new Date();
exp.setTime(exp.getTime() + Days * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString() + ";path=/";
}
s_theme=getCookie("_theme");

View File

@@ -1,5 +1,82 @@
# 更新日志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
- 修复:终端交互导致的 ssh 断掉后 CPU 占用过高的问题
- 修复WorkerCache 在缺少配置文件下工作异常的问题
- 新增:全局函数:`zm_atomic()`
## v2.2.3
> 更新时间2021.1.30
- 修复waitMessage() 在 v2.2.2 版本中不可用的 bug
- 修复access_token 无效的问题
## v2.2.2
> 更新时间2021.1.29
- 修复:模块文件错误时避免循环报错
- 优化:代码结构
- 修复:在不同进程时调用机器人 API 无法返回且报错的 bug
- **修复机器人无法连接的问题2.1.6 ~ 2.2.1 受影响)**
## v2.2.1
> 更新时间2021.1.29
- 修复:配置文件兼容性问题
## v2.2.0
> 更新时间2021.1.29

View File

@@ -10,8 +10,8 @@ theme:
favicon: assets/favicon.png
language: zh
palette:
primary: blue
accent: blue
primary: indigo
accent: indigo
features:
- navigation.tabs
extra_javascript:
@@ -34,7 +34,7 @@ extra:
version:
method: mike
copyright: 'Copyright &copy; 2019 - 2020 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
copyright: 'Copyright &copy; 2019 - 2021 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</span>
@@ -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

View File

@@ -1,31 +0,0 @@
<?php
namespace Custom\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CustomCommand extends Command
{
// the name of the command (the part after "bin/console")
protected static $defaultName = 'custom';
protected function configure() {
$this->setDescription("custom description | 自定义命令的描述字段");
$this->addOption("failure", null, null, "以错误码为1返回结果");
// ...
}
protected function execute(InputInterface $input, OutputInterface $output) {
if ($input->getOption("failure")) {
$output->writeln("<error>Hello error! I am wrong message.</error>");
return Command::FAILURE;
} else {
$output->writeln("<comment>Hello world! I am successful message.</comment>");
return Command::SUCCESS;
}
}
}

View File

@@ -1,4 +1,4 @@
<?php #plain
<?php /** @noinspection PhpFullyQualifiedNameUsageInspection */ #plain
//这里写你的全局函数
function pgo(callable $func, $name = "default") {

View File

@@ -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"];
}
/**

View File

@@ -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("&amp;", "&", $str);
$str = str_replace("&#91;", "[", $str);
$str = str_replace("&#93;", "]", $str);
return $str;
public static function decode($msg, $is_content = false) {
$msg = str_replace(["&amp;", "&#91;", "&#93;"], ["&", "[", "]"], $msg);
if ($is_content) $msg = str_replace("&#44;", ",", $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("&", "&amp;", $msg);
$msg = str_replace("[", "&#91;", $msg);
$msg = str_replace("]", "&#93;", $msg);
public static function escape($msg, $is_content = false) {
$msg = str_replace(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $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(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $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;
}
}

View File

@@ -3,17 +3,16 @@
namespace ZM\API;
use Co;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Store\ZMAtomic;
use ZM\Utils\CoMessage;
trait CQAPI
{
/**
* @param ConnectionObject $connection
* @param $connection
* @param $reply
* @param |null $function
* @return bool|array
@@ -35,21 +34,20 @@ trait CQAPI
$r[$api_id] = [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getOption("connect_id")
"self_id" => $connection->getOption("connect_id"),
"echo" => $api_id
];
if ($function === true) $r[$api_id]["coroutine"] = Co::getuid();
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
if (server()->push($connection->getFd(), json_encode($reply))) {
if ($function === true) {
Co::suspend();
return CoMessage::yieldByWS($r[$api_id], ["echo"], 60);
} else {
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
$data = $r[$api_id];
unset($r[$api_id]);
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
return isset($data['result']) ? $data['result'] : null;
}
return true;
} else {

View File

@@ -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;
@@ -228,9 +228,9 @@ class ZMRobot
/**
* 群组单人禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
* @param int $group_id
* @param int $user_id
* @param int $duration
* @param $group_id
* @param $user_id
* @param $duration
* @return array|bool|null
*/
public function setGroupBan($group_id, $user_id, $duration = 1800) {

View File

@@ -9,11 +9,10 @@ use ZM\Console\Console;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ZM\Annotation\Http\{HandleAfter, HandleBefore, Controller, HandleException, Middleware, MiddlewareClass, RequestMapping};
use ZM\Annotation\Http\{HandleAfter, HandleBefore, HandleException, Middleware, MiddlewareClass, RequestMapping};
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Module\Closed;
use ZM\Http\RouteManager;
use ZM\Utils\DataProvider;
class AnnotationParser
{
@@ -48,7 +47,7 @@ class AnnotationParser
*/
public function registerMods() {
foreach ($this->path_list as $path) {
Console::debug("parsing annotation in ".$path[0]);
Console::debug("parsing annotation in " . $path[0]);
$all_class = getAllClasses($path[0], $path[1]);
$this->reader = new AnnotationReader();
foreach ($all_class as $v) {
@@ -91,6 +90,7 @@ class AnnotationParser
if ($vs instanceof ErgodicAnnotation) {
foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) {
$copy = clone $vs;
/** @noinspection PhpUndefinedFieldInspection */
$copy->method = $method->getName();
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
}

View File

@@ -3,7 +3,6 @@
namespace ZM\Annotation\CQ;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;

View File

@@ -4,7 +4,6 @@
namespace ZM\Annotation\Http;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;

View File

@@ -5,7 +5,6 @@ namespace ZM\Annotation\Swoole;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\Interfaces\Rule;
/**
* @Annotation

View File

@@ -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();

View File

@@ -6,7 +6,6 @@ namespace ZM\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Utils\DataProvider;
class DaemonStatusCommand extends DaemonCommand
{

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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();
}
/**
@@ -145,7 +153,7 @@ class Context implements ContextInterface
if ($prompt != "") $this->reply($prompt);
try {
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())]);
$r = CoMessage::yieldByWS($this->getData(), ["user_id", "self_id", "message_type", onebot_target_id_name($this->getMessageType())], $timeout);
} catch (Exception $e) {
$r = false;
}

View File

@@ -31,7 +31,6 @@ class DB
/**
* @param $table_name
* @param bool $enable_cache
* @return Table
* @throws DbException
*/
@@ -39,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");
@@ -85,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) {
@@ -95,6 +95,7 @@ class DB
$ps = $conn->prepare($line);
if ($ps === false) {
SqlPoolStorage::$sql_pool->put(null);
/** @noinspection PhpUndefinedFieldInspection */
throw new DbException("SQL语句查询错误" . $line . ",错误信息:" . $conn->error);
} else {
if (!($ps instanceof PDOStatement) && !($ps instanceof PDOStatementProxy)) {
@@ -115,7 +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);

View File

@@ -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);
}
}

View File

@@ -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($line){
public function statement() {
$this->cache = [];
//TODO: 无返回的statement语句
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -87,27 +87,23 @@ class EventDispatcher
/**
* @param mixed ...$params
* @throws Exception
* @throws InterruptException
*/
public function dispatchEvents(...$params) {
try {
foreach ((EventManager::$events[$this->class] ?? []) as $v) {
$this->dispatchEvent($v, $this->rule, ...$params);
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 分发结束。");
if($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) continue;
if (is_callable($this->return_func) && $this->status === self::STATUS_NORMAL) {
if ($this->log) Console::verbose("[事件分发{$this->eid}] 单一对象 " . $v->class . "::" . $v->method . " 正在执行返回值处理函数 ...");
($this->return_func)($this->store);
}
}
if($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
if ($this->status === self::STATUS_RULE_FAILED) $this->status = self::STATUS_NORMAL;
} catch (InterruptException $e) {
$this->store = $e->return_var;
$this->status = self::STATUS_INTERRUPTED;
} catch (Exception $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
} catch (Error $e) {
} catch (Exception | Error $e) {
$this->status = self::STATUS_EXCEPTION;
throw $e;
}
@@ -117,9 +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;

View File

@@ -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();
});
}
}
}

View File

@@ -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;
@@ -62,7 +64,12 @@ class ServerEventHandler
if ($terminal_id !== null) {
ZMBuf::$terminal = $r = STDIN;
Event::add($r, function () use ($r) {
$var = trim(fgets($r));
$fget = fgets($r);
if ($fget === false) {
Event::del($r);
return;
}
$var = trim($fget);
try {
Terminal::executeCommand($var, $r);
} catch (Exception $e) {
@@ -73,18 +80,23 @@ class ServerEventHandler
});
}
Process::signal(SIGINT, function () use ($r) {
echo "\r";
Console::warning("Server interrupted by keyboard on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
if (zm_atomic("_int_is_reload")->get() === 1) {
zm_atomic("_int_is_reload")->set(0);
ZMUtil::reload();
} else {
echo "\r";
Console::warning("Server interrupted(SIGINT) on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
}
});
if(Framework::$argv["daemon"]) {
if (Framework::$argv["daemon"]) {
$daemon_data = json_encode([
"pid" => \server()->master_pid,
"stdout" => ZMConfig::get("global")["swoole"]["log_file"]
],128|256);
file_put_contents(DataProvider::getWorkingDir()."/.daemon_pid", $daemon_data);
], 128 | 256);
file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data);
}
if (Framework::$argv["watch"]) {
if (extension_loaded('inotify')) {
@@ -116,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 已停止");
}
@@ -238,7 +253,7 @@ class ServerEventHandler
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
Console::error("Maybe it caused by your own code if in your own Module directory.");
Console::log($e->getTraceAsString(), 'gray');
ZMUtil::stop();
posix_kill($server->master_pid, SIGINT);
}
} else {
// 这里是TaskWorker初始化的内容部分
@@ -255,7 +270,7 @@ class ServerEventHandler
Console::error("PHP Error: " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());
Console::error("Maybe it caused by your own code if in your own Module directory.");
Console::log($e->getTraceAsString(), 'gray');
ZMUtil::stop();
posix_kill($server->master_pid, SIGINT);
}
}
}
@@ -314,7 +329,7 @@ class ServerEventHandler
*/
public function onRequest(?Request $request, ?\Swoole\Http\Response $response) {
$response = new Response($response);
foreach(ZMConfig::get("global")["http_header"] as $k => $v) {
foreach (ZMConfig::get("global")["http_header"] as $k => $v) {
$response->setHeader($k, $v);
}
unset(Context::$context[Co::getCid()]);
@@ -401,6 +416,21 @@ class ServerEventHandler
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
unset(Context::$context[Co::getCid()]);
$type = strtolower($request->header["x-client-role"] ?? $request->get["type"] ?? "");
$access_token = explode(" ", $request->header["authorization"] ?? "")[1] ?? $request->get["token"] ?? "";
$token = ZMConfig::get("global", "access_token");
if ($token instanceof Closure) {
if (!$token($access_token)) {
$server->close($request->fd);
Console::warning("Unauthorized access_token: " . $access_token);
return;
}
} elseif (is_string($token)) {
if ($access_token !== $token && $token !== "") {
$server->close($request->fd);
Console::warning("Unauthorized access_token: " . $access_token);
return;
}
}
$type_conn = ManagerGM::getTypeClassName($type);
ManagerGM::pushConnect($request->fd, $type_conn);
$conn = ManagerGM::get($request->fd);
@@ -491,16 +521,20 @@ class ServerEventHandler
/**
* @SwooleHandler("pipeMessage")
* @param $server
* @param Server $server
* @param $src_worker_id
* @param $data
* @throws InterruptException
* @throws Exception
*/
public function onPipeMessage(Server $server, $src_worker_id, $data) {
//var_dump($data, $server->worker_id);
//unset(Context::$context[Co::getCid()]);
$data = json_decode($data, true);
switch ($data["action"] ?? '') {
case "resume_ws_message":
$obj = $data["data"];
Co::resume($obj["coroutine"]);
break;
case "getWorkerCache":
$r = WorkerCache::get($data["key"]);
$action = ["action" => "returnWorkerCache", "cid" => $data["cid"], "value" => $r];
@@ -516,6 +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;
@@ -557,10 +596,11 @@ class ServerEventHandler
* @param Server|null $server
* @param Server\Task $task
* @return mixed
* @noinspection PhpUnusedParameterInspection
*/
public function onTask(?Server $server, Server\Task $task) {
$data = $task->data;
switch($data["action"]) {
switch ($data["action"]) {
case "runMethod":
$c = $data["class"];
$ss = new $c();

View File

@@ -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];
}

View File

@@ -4,8 +4,6 @@
namespace ZM\Http;
use ZM\Console\Console;
class Response
{
@@ -162,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 {

View File

@@ -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);

View File

@@ -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));

View File

@@ -3,7 +3,7 @@
namespace ZM\Module;
use Swoole\Coroutine;
use Exception;
use ZM\Annotation\CQ\CQAPIResponse;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQCommand;
@@ -14,8 +14,6 @@ use ZM\Annotation\CQ\CQRequest;
use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException;
use ZM\Exception\WaitTimeoutException;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
use ZM\Utils\CoMessage;
/**
@@ -26,27 +24,27 @@ class QQBot
{
/**
* @throws InterruptException
* @throws Exception
*/
public function handle() {
try {
$data = json_decode(context()->getFrame()->data, true);
set_coroutine_params(["data" => $data]);
if (isset($data["post_type"])) {
//echo TermColor::ITALIC.json_encode($data, 128|256).TermColor::RESET.PHP_EOL;
set_coroutine_params(["data" => $data]);
ctx()->setCache("level", 0);
//Console::debug("Calling CQ Event from fd=" . ctx()->getConnection()->getFd());
if ($data["post_type"] != "meta_event") {
$r = $this->dispatchBeforeEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
if (CoMessage::resumeByWS()) {
EventDispatcher::interrupt();
}
//Console::warning("最上数据包:".json_encode($data));
$this->dispatchEvents($data);
} else {
$this->dispatchAPIResponse($data);
}
if (isset($data["echo"]) || isset($data["post_type"])) {
if (CoMessage::resumeByWS()) EventDispatcher::interrupt();
}
if (isset($data["post_type"])) $this->dispatchEvents($data);
else $this->dispatchAPIResponse($data);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
$e->module->finalReply($e->getMessage());
}
@@ -55,7 +53,7 @@ class QQBot
/**
* @param $data
* @return EventDispatcher
* @throws InterruptException
* @throws Exception
*/
public function dispatchBeforeEvents($data) {
$before = new EventDispatcher(CQBefore::class);
@@ -177,45 +175,16 @@ class QQBot
}
}
/**
* @param $req
* @throws Exception
*/
private function dispatchAPIResponse($req) {
$status = $req["status"];
$retcode = $req["retcode"];
$data = $req["data"];
if (isset($req["echo"]) && is_numeric($req["echo"])) {
$r = LightCacheInside::get("wait_api", "wait_api");
if (isset($r[$req["echo"]])) {
$origin = $r[$req["echo"]];
$self_id = $origin["self_id"];
$response = [
"status" => $status,
"retcode" => $retcode,
"data" => $data,
"self_id" => $self_id,
"echo" => $req["echo"]
];
set_coroutine_params(["cq_response" => $response]);
$dispatcher = new EventDispatcher(CQAPIResponse::class);
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
return $response->retcode == ctx()->getCQResponse()["retcode"];
});
$dispatcher->dispatchEvents($response);
$origin_ctx = ctx()->copy();
set_coroutine_params($origin_ctx);
if (($origin["coroutine"] ?? false) !== false) {
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
$r[$req["echo"]]["result"] = $response;
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
Coroutine::resume($origin['coroutine']);
}
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
unset($r[$req["echo"]]);
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
}
}
set_coroutine_params(["cq_response" => $req]);
$dispatcher = new EventDispatcher(CQAPIResponse::class);
$dispatcher->setRuleFunction(function (CQAPIResponse $response) {
return $response->retcode == ctx()->getCQResponse()["retcode"];
});
$dispatcher->dispatchEvents($req);
}
}

View File

@@ -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) {

View File

@@ -23,9 +23,10 @@ class LightCacheInside
$result = self::$kv_table["wait_api"]->create() && self::$kv_table["connect"]->create();
if ($result === false) {
self::$last_error = '系统内存不足,申请失败';
return $result;
return false;
} else {
return true;
}
return $result;
}
/**

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -15,7 +15,7 @@ class WorkerCache
public static $transfer = [];
public static function get($key) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
return self::$store[$key] ?? null;
} else {
@@ -29,72 +29,68 @@ class WorkerCache
}
public static function set($key, $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
self::$store[$key] = $value;
return true;
} else {
$action = ["action" => $async ? "asyncSetWorkerCache" : "setWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
return self::processRemote($action, $async, $config);
}
}
public static function hasKey($key, $subkey) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
return isset(self::$store[$key][$subkey]);
} else {
$action = ["hasKeyWorkerCache", "key" => $key, "subkey" => $subkey, "cid" => zm_cid()];
return self::processRemote($action, false, $config);
}
}
private static function processRemote($action, $async, $config) {
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
if (!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
}
public static function unset($key, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
unset(self::$store[$key]);
return true;
} else {
$action = ["action" => $async ? "asyncUnsetWorkerCache" : "unsetWorkerCache", "key" => $key, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
return self::processRemote($action, $async, $config);
}
}
public static function add($key, int $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
if(!isset(self::$store[$key])) self::$store[$key] = 0;
if (!isset(self::$store[$key])) self::$store[$key] = 0;
self::$store[$key] += $value;
return true;
} else {
$action = ["action" => $async ? "asyncAddWorkerCache" : "addWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
// if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
return self::processRemote($action, $async, $config);
}
}
public static function sub($key, int $value, $async = false) {
$config = self::$config ?? ZMConfig::get("global", "worker_cache");
$config = self::$config ?? ZMConfig::get("global", "worker_cache") ?? ["worker" => 0];
if ($config["worker"] === server()->worker_id) {
if(!isset(self::$store[$key])) self::$store[$key] = 0;
if (!isset(self::$store[$key])) self::$store[$key] = 0;
self::$store[$key] -= $value;
return true;
} else {
$action = ["action" => $async ? "asyncSubWorkerCache" : "subWorkerCache", "key" => $key, "value" => $value, "cid" => zm_cid()];
$ss = server()->sendMessage(json_encode($action, JSON_UNESCAPED_UNICODE), $config["worker"]);
// if(!$ss) return false;
if ($async) return true;
zm_yield();
$p = self::$transfer[zm_cid()] ?? null;
unset(self::$transfer[zm_cid()]);
return $p;
return self::processRemote($action, $async, $config);
}
}
}

View File

@@ -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) {

View File

@@ -16,7 +16,7 @@ class CoMessage
* @param array $hang
* @param array $compare
* @param int $timeout
* @return bool
* @return mixed
* @throws Exception
*/
public static function yieldByWS(array $hang, array $compare, $timeout = 600) {
@@ -48,4 +48,35 @@ class CoMessage
if ($result === null) return false;
return $result;
}
public static function resumeByWS() {
$dat = ctx()->getData();
$last = null;
SpinLock::lock("wait_api");
$all = LightCacheInside::get("wait_api", "wait_api") ?? [];
foreach ($all as $k => $v) {
if (!isset($v["compare"])) continue;
foreach ($v["compare"] as $vs) {
if (!isset($v[$vs], $dat[$vs])) continue 2;
if ($v[$vs] != $dat[$vs]) {
continue 2;
}
}
$last = $k;
}
if ($last !== null) {
$all[$last]["result"] = $dat;
LightCacheInside::set("wait_api", "wait_api", $all);
SpinLock::unlock("wait_api");
if ($all[$last]["worker_id"] != server()->worker_id) {
ZMUtil::sendActionToWorker($all[$last]["worker_id"], "resume_ws_message", $all[$last]);
} else {
Co::resume($all[$last]["coroutine"]);
}
return true;
} else {
SpinLock::unlock("wait_api");
return false;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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) {
@@ -51,4 +60,8 @@ class ZMUtil
return ZMBuf::$instance[$class];
}
}
public static function sendActionToWorker($target_id, $action, $data) {
server()->sendMessage(json_encode(["action" => $action, "data" => $data]), $target_id);
}
}

View File

@@ -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);

View File

@@ -24,6 +24,7 @@ function phar_classloader($p) {
return;
}
try {
/** @noinspection PhpIncludeInspection */
require_once $filepath;
} catch (Exception $e) {
echo "Error when finding class: " . $p . PHP_EOL;
@@ -75,7 +76,7 @@ function unicode_decode($str) {
/**
* 获取模块文件夹下的每个类文件的类名称
* @param $dir
* @param string $indoor_name
* @param $indoor_name
* @return array
*/
function getAllClasses($dir, $indoor_name) {
@@ -88,13 +89,14 @@ function getAllClasses($dir, $indoor_name) {
//echo "At " . $indoor_name . PHP_EOL;
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
elseif (mb_substr($v, -4) == ".php") {
if(substr(file_get_contents($dir.$v), 6, 6) == "#plain") continue;
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir()."/composer.json"), true);
foreach($composer["autoload"]["files"] as $fi) {
if(realpath(DataProvider::getWorkingDir()."/".$fi) == realpath($dir.$v)) {
if (substr(file_get_contents($dir . $v), 6, 6) == "#plain") continue;
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
foreach ($composer["autoload"]["files"] as $fi) {
if (realpath(DataProvider::getWorkingDir() . "/" . $fi) == realpath($dir . $v)) {
continue 2;
}
}
if ($v == "global_function.php") continue;
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
$classes [] = $class_name;
}
@@ -265,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) {
@@ -308,3 +314,26 @@ function getAllFdByConnectType(string $type = 'default'): array {
}
return $fds;
}
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;
}