Compare commits

...

17 Commits
2.5.7 ... 2.6.2

Author SHA1 Message Date
Jerry Ma
176b690417 Update v2.md 2021-12-07 13:28:21 +08:00
Jerry Ma
e22b1b90ec Update build-update.md 2021-12-07 13:27:33 +08:00
Jerry Ma
a3c560790c Merge pull request #55 from zhamao-robot/fix/cqafter
update to build 429
2021-12-07 13:25:55 +08:00
570e2108dd - 新增配置项 onebot.message_command_policy
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
2021-12-07 13:21:29 +08:00
Jerry Ma
59c0d95e5d Create 2_Feature_request.yaml 2021-12-07 12:34:18 +08:00
Jerry Ma
9ceaecdc02 Update README.md 2021-12-07 12:17:47 +08:00
Jerry Ma
19d50898ef Create 1_Bug_report.yaml 2021-12-07 12:15:49 +08:00
Jerry Ma
5b62ca62ae Update README.md 2021-11-17 00:28:54 +08:00
fb528d30ce update docs 2021-11-16 16:51:05 +08:00
5f2d5ed334 update to build 428 2021-11-16 16:49:32 +08:00
c20e459900 update docs 2021-11-16 15:44:34 +08:00
09220825cf update to build 427 2021-11-16 15:41:01 +08:00
Jerry Ma
3d4db23d27 Update v2.md 2021-11-10 14:10:08 +08:00
Jerry Ma
4496b67dcc Update build-update.md 2021-11-10 14:09:18 +08:00
Jerry Ma
2a13298384 update to 2.5.8 (build 426) 2021-11-10 14:07:21 +08:00
Jerry Ma
d6ec404d76 Merge pull request #52 from YuFengZe/master
Update CQ.php
2021-11-10 14:06:13 +08:00
YuFengZe
3235fd4dc1 Update CQ.php 2021-11-08 22:05:41 +08:00
30 changed files with 642 additions and 60 deletions

View File

@@ -0,0 +1,38 @@
name: 🐛 漏洞BUG报告
description: ⚠️ 请不要直接在此提交安全漏洞
labels: bug
body:
- type: input
id: affected-versions
attributes:
label: 受影响版本
placeholder: x.y.z
validations:
required: true
- type: textarea
id: description
attributes:
label: 描述
description: 请详细地描述您的问题
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: 复现步骤
description: |
请尽可能地提供可以复现此步骤的漏洞。
如果步骤过长或难以描述,您可以自行建立一个用于复现漏洞的仓库。
validations:
required: true
- type: textarea
id: possible-solution
attributes:
label: 解决方案
description: 如果您对这个漏洞的成因或修复有任何意见的话,请在此提出
- type: textarea
id: additional-context
attributes:
label: 附加信息
description: 其他可能有帮助的信息,如日志、截图等

View File

@@ -0,0 +1,19 @@
name: 🚀 功能建议
description: 新功能、改进的意见、草案
labels: enhancement
body:
- type: textarea
id: description
attributes:
label: 描述
description: 请提供简洁清楚的描述
validations:
required: true
- type: textarea
id: example
attributes:
label: 例子
description: |
一个简单的例子,展示该功能将如何被使用(包括代码、配置文件等)
如果这是针对已有功能的改进,请展示改进前后使用方式(或效能)的对比

View File

@@ -110,4 +110,10 @@ vendor/bin/start server
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)
感谢 JetBrains 为此开源项目提供 PhpStorm 开发工具支持:
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/PhpStorm.svg" width="300">
感谢 [php-libonebot](https://github.com/botuniverse/php-libonebot) 开发者 @sunxyw 中为项目开发规范化提出的一些建议。
<!-- ![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg) -->

View File

@@ -39,7 +39,9 @@ $config['swoole'] = [
$config['runtime'] = [
'swoole_coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL),
'swoole_server_mode' => SWOOLE_PROCESS,
'middleware_error_policy' => 1
'middleware_error_policy' => 1,
'reload_delay_time' => 800,
'global_middleware_binding' => []
];
/** 轻量字符串缓存,默认开启 */
@@ -120,7 +122,9 @@ $config['static_file_server'] = [
$config['onebot'] = [
'status' => true,
'single_bot_mode' => false,
'message_level' => 99
'message_level' => 99,
'message_convert_string' => true,
'message_command_policy' => 'interrupt',
];
/** 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */

View File

@@ -128,3 +128,52 @@ public function onStart() {
( 其实还是很聪明的!
</chat-box>
### strToArray()
`string` 类型的消息文本转换为 `array` 格式。
定义:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
参数 `$msg` 为带 OB/CQ 码的字符串消息,如 `你好啊,[CQ:at,qq=123]`
参数 `$ignore_space``false` 时,转换的数组内会包含空 `text` 段。
参数 `$trim_text``true` 时,会自动去除 `text` 段消息头尾的换行符和空格。
这个命令转换的数组格式符合 OneBot 11/12 标准,但细节上可能会与不同 OneBot 实现有所差异。
```php
$str = "你好啊,[CQ:at,qq=123]";
$arr = \ZM\Utils\MessageUtil::strToArray($str);
```
转换结果参考如下:
```json
[
{
"type": "text",
"data": {
"text": "你好啊,"
}
},
{
"type": "at",
"data": {
"qq": "123"
}
}
]
```
### arrayToStr()
`array` 格式的消息内容转换为字符串 + CQ 码的形式。
定义:`arrayToStr(array $array)`
```php
// 我们使用上边的 $arr 作为传入值。
$new_str = \ZM\Utils\MessageUtil::arrayToStr($arr);
// 结果:"你好啊,[CQ:at,qq=123]"
```

View File

@@ -0,0 +1,104 @@
# 事件跟踪器及调试
众所周知炸毛框架中的事件由内置的事件分发器EventDispatcher负责分发但调试事件分发在之前的版本比较困难例如不能获取到事件如何被调用以及事件如何被捕获。
EventTracer 的作用是记录事件的调用顺序,以便于调试。
命名空间使用指南:`use ZM\Event\EventTracer;`
## EventTracer::getCurrentEvent() - 获取当前注解事件对象
```php
/**
* @OnStart()
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEvent());
}
/*
^ ZM\Annotation\Swoole\OnStart^ {#192
+worker_id: 0
+method: "onStart"
+class: "Module\Example\Hello"
}
*/
```
这里这个方法必须在注解事件内执行,如果在注解事件外执行,将会返回 `null`
## EventTracer::getCurrentEventMiddlewares() - 获取当前注解事件的中间件们
```php
/**
* @OnStart()
* @Middleware("timer")
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEventMiddlewares());
}
/*
^ array:1 [
0 => ZM\Annotation\Http\Middleware^ {#194
+middleware: "timer"
+params: []
+method: "onStart"
+class: "Module\Example\Hello"
}
]
*/
```
返回值为当前注解事件的中间件们,如果没有注解中间件,返回 `[]`
## EventTracer::getEventTraceList() - 获取注解事件的列表
此处返回的是 `getCurrentEvent()` 相同的对象,但是返回的是一个数组,数组中的元素是注解事件。
```php
/**
* 一个简单随机数的功能demo
* 问法1随机数 1 20
* 问法2从1到20的随机数
* @CQCommand("随机数")
* @Middleware("timer")
* @CQCommand(pattern="*从*到*的随机数")
* @return string
*/
public function randNum() {
// 此处为随机数代码
zm_dump(EventTracer::getEventTraceList());
return "随机数:" . rand(1, 20);
}
/*
^ array:2 [
0 => ZM\Annotation\CQ\CQCommand^ {#193
+match: ""
+pattern: "*从*到*的随机数"
+regex: ""
+start_with: ""
+end_with: ""
+keyword: ""
+alias: []
+message_type: ""
+user_id: 0
+group_id: 0
+discuss_id: 0
+level: 20
+method: "randNum"
+class: "Module\Example\Hello"
}
1 => ZM\Annotation\Swoole\OnMessageEvent^ {#165
+connect_type: "default"
+rule: "connectIsQQ()"
+level: 99
+method: "handleByEvent"
+class: "ZM\Module\QQBot"
}
]
*/
```
## EventDispatcher::enableEventTrace() - 启用事件跟踪器
还没写完,不着急。

View File

@@ -56,6 +56,8 @@
| `swoole_coroutine_hook_flags` | Swoole 启动时一键协程化 Hook 的 Flag 值,详见 [一键协程化](http://wiki.swoole.com/#/runtime?id=%e5%87%bd%e6%95%b0%e5%8e%9f%e5%9e%8b) | `SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL)` |
| `swoole_server_mode` | Swoole Server 启动的进程模式,有 `SWOOLE_PROCESS``SWOOLE_BASE` 两种,见 [启动方式](http://wiki.swoole.com/#/learn?id=swoole_process) | `SWOOLE_PROCESS` |
| `middleware_error_policy` | 中间件错误处理策略,见 [中间件 - 错误处理策略](../../event/middleware/#_6) | 1 |
| `reload_delay_time` | 框架 reload 重载命令接收后延迟的时间毫秒0 为不等待) | 800 |
| `global_middleware_binding` | 给注解事件绑定全局中间件,见 [中间件 - 全局中间件](../../event/middleware/#_6) | `[]` |
### 子表 **light_cache**
@@ -106,11 +108,13 @@
### 子表 onebot
| 配置名称 | 说明 | 默认值 |
| ----------------- | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99999 |
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99 |
| `message_convert_string` | 是否将数组格式的消息转换为字符串以保证与旧版本的兼容性 | true |
| `message_command_policy` | CQCommand命令匹配后执行流程`interrupt` 为不执行后续 CQMessage`continue` 为继续 | `interrupt` |
### 子表 remote_terminal

View File

@@ -4,6 +4,37 @@
同时此处将只使用 build 版本号进行区分。
## build 429 (2021-12-7)
- 新增配置项 `onebot`.`message_command_policy`
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
## build 428 (2021-11-16)
- 修复 `ctx()->waitMessage()` 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 `ctx()->getArrayMessage()``ctx()->getStringMessage()` 两个方法
- 修复注解事件 `CQCommand``CQMessage` 在 array 消息模式下无法正确解析的 Bug
## build 427 (2021-11-16)
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## build 426 (2021-11-10)
- 修复 CQ 码的解析函数 Bug#52
## build 425 (2021-11-3)
- 删除未实际应用功能的配置参数

View File

@@ -2,6 +2,11 @@
这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。
## v2.6.0 (build 427)
- 新增 `$config['runtime']` 下的 `reload_delay_time``global_middleware_binding` 项。
- 新增 `$config['onebot']` 下的 `message_convert_string` 项。
## v2.5.1 (build 417)
- 新增 `$config['runtime']` 下的 `middleware_error_policy` 选项。

View File

@@ -1,5 +1,44 @@
# 更新日志v2 版本)
## v2.6.2 (build 429)
> 更新时间2021.12.7
- 新增配置项 `onebot`.`message_command_policy`
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
## v2.6.1 (build 428)
> 更新时间2021.11.16
- 修复 ctx()->waitMessage() 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 ctx()->getArrayMessage() 和 ctx()->getStringMessage() 两个方法
- 修复注解事件 CQCommand 和 CQMessage 在 array 消息模式下无法正确解析的 Bug
## v2.6.0 (build 427)
> 更新时间2021.11.16
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## v2.5.8 (build 426)
> 更新时间2021.11.10
- 修复 CQ 码的解析函数 Bug#52
## v2.5.7 (build 425)
> 更新时间2021.11.3
@@ -442,4 +481,4 @@
> 更新时间2020.12.23
已发布正式版。
已发布正式版。

View File

@@ -33,7 +33,7 @@ class TimerMiddleware implements MiddlewareInterface
* @HandleAfter()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 3) . " ms.");
}
/**
@@ -42,7 +42,7 @@ class TimerMiddleware implements MiddlewareInterface
* @throws Exception
*/
public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 3) . " ms but an Exception occurred.");
throw $e;
}
}

View File

@@ -356,7 +356,7 @@ class CQ
}
$cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1;
$offset += $head + $tmpmsg + 1;
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
}
return $cqs;

View File

@@ -3,6 +3,7 @@
namespace ZM\API;
use Closure;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
@@ -11,6 +12,13 @@ use ZM\Utils\CoMessage;
trait CQAPI
{
/** @var null|Closure */
private static $filter = null;
public static function registerFilter(callable $callable) {
self::$filter = $callable;
}
/**
* @param $connection
* @param $reply
@@ -18,6 +26,11 @@ trait CQAPI
* @return bool|array
*/
private function processAPI($connection, $reply, $function = null) {
if (is_callable(self::$filter)) {
$reply2 = call_user_func(self::$filter, $reply);
if (is_bool($reply2)) return $reply2;
else $reply = $reply2;
}
if ($connection->getOption("type") === CONN_WEBSOCKET)
return $this->processWebsocketAPI($connection, $reply, $function);
else

View File

@@ -4,6 +4,7 @@
namespace ZM\Annotation;
use Doctrine\Common\Annotations\AnnotationReader;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
@@ -98,7 +99,6 @@ 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;
}
@@ -116,12 +116,27 @@ class AnnotationParser
}
}
$inserted = [];
//预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach (($this->annotation_map[$v]["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
foreach ($methods_annotations as $method_anno) {
/** @var AnnotationBase $method_anno */
$method_anno->class = $v;
$method_anno->method = $method_name;
if (!($method_anno instanceof Middleware) && ($middlewares = ZMConfig::get("global", "runtime")["global_middleware_binding"][get_class($method_anno)] ?? []) !== []) {
if (!isset($inserted[$v][$method_name])) {
// 在这里在其他中间件前插入插入全局的中间件
foreach ($middlewares as $middleware) {
$mid_class = new Middleware();
$mid_class->middleware = $middleware;
$mid_class->class = $v;
$mid_class->method = $method_name;
$this->middleware_map[$v][$method_name][] = $mid_class;
}
$inserted[$v][$method_name] = true;
}
}
if ($method_anno instanceof RequestMapping) {
RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations);
} elseif ($method_anno instanceof Middleware) {
@@ -134,9 +149,10 @@ class AnnotationParser
Console::debug("解析注解完毕!");
}
public function generateAnnotationEvents() {
public function generateAnnotationEvents(): array {
$o = [];
foreach ($this->annotation_map as $obj) {
// 这里的ErgodicAnnotation是为了解决类上的注解可穿透到方法上的问题
foreach (($obj["class_annotations"] ?? []) as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) continue;
else $o[get_class($class_annotation)][] = $class_annotation;

View File

@@ -12,7 +12,7 @@ class DaemonReloadCommand extends DaemonCommand
protected static $defaultName = 'daemon:reload';
protected function configure() {
$this->setDescription("重载守护进程下的用户代码(仅限--daemon模式可用");
$this->setDescription("重载框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {

View File

@@ -11,18 +11,20 @@ class DaemonStatusCommand extends DaemonCommand
protected static $defaultName = 'daemon:status';
protected function configure() {
$this->setDescription("查看守护进程框架的运行状态(仅限--daemon模式可用");
$this->setDescription("查看框架的运行状态");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
$output->writeln("<info>框架运行中pid" . $this->daemon_file["pid"] . "</info>");
$output->writeln("<comment>----- 以下是stdout内容 -----</comment>");
$stdout = file_get_contents($this->daemon_file["stdout"]);
$stdout = explode("\n", $stdout);
for ($i = 15; $i > 0; --$i) {
if (isset($stdout[count($stdout) - $i]))
echo $stdout[count($stdout) - $i] . PHP_EOL;
$output->writeln("<info>框架" . ($this->daemon_file["daemon"] ? "以守护进程模式" : "") . "运行中pid" . $this->daemon_file["pid"] . "</info>");
if ($this->daemon_file["daemon"]) {
$output->writeln("<comment>----- 以下是stdout内容 -----</comment>");
$stdout = file_get_contents($this->daemon_file["stdout"]);
$stdout = explode("\n", $stdout);
for ($i = 15; $i > 0; --$i) {
if (isset($stdout[count($stdout) - $i]))
echo $stdout[count($stdout) - $i] . PHP_EOL;
}
}
return 0;
}

View File

@@ -13,7 +13,7 @@ class DaemonStopCommand extends DaemonCommand
protected static $defaultName = 'daemon:stop';
protected function configure() {
$this->setDescription("停止守护进程下运行的框架(仅限--daemon模式可用");
$this->setDescription("停止运行的框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {

View File

@@ -8,12 +8,14 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Framework;
use ZM\Utils\DataProvider;
class RunServerCommand extends Command
{
protected static $defaultName = 'server';
protected function configure() {
$this->setAliases(['server:start']);
$this->setDefinition([
new InputOption("debug-mode", "D", null, "开启调试模式 (这将关闭协程化)"),
new InputOption("log-debug", null, null, "调整消息等级到debug (log-level=4)"),
@@ -47,6 +49,17 @@ class RunServerCommand extends Command
return 1;
}
}
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
if (file_exists($pid_path)) {
$pid = json_decode(file_get_contents($pid_path), true)["pid"] ?? null;
if ($pid !== null && posix_getsid($pid) !== false) {
$output->writeln("<error>检测到已经在 pid: $pid 进程启动了框架!</error>");
$output->writeln("<error>不可以同时启动两个框架!</error>");
return 1;
} else {
unlink($pid_path);
}
}
(new Framework($input->getOptions()))->start();
return 0;
}

View File

@@ -0,0 +1,25 @@
<?php
namespace ZM\Command\Server;
use Swoole\Process;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Daemon\DaemonCommand;
class ServerReloadCommand extends DaemonCommand
{
protected static $defaultName = 'server:reload';
protected function configure() {
$this->setDescription("重载框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
Process::kill(intval($this->daemon_file["pid"]), SIGUSR1);
$output->writeln("<info>成功重载!</info>");
return 0;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace ZM\Command\Server;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Daemon\DaemonCommand;
class ServerStatusCommand extends DaemonCommand
{
protected static $defaultName = 'server:status';
protected function configure() {
$this->setDescription("查看框架的运行状态");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
$output->writeln("<info>框架" . ($this->daemon_file["daemon"] ? "以守护进程模式" : "") . "运行中pid" . $this->daemon_file["pid"] . "</info>");
if ($this->daemon_file["daemon"]) {
$output->writeln("<comment>----- 以下是stdout内容 -----</comment>");
$stdout = file_get_contents($this->daemon_file["stdout"]);
$stdout = explode("\n", $stdout);
for ($i = 15; $i > 0; --$i) {
if (isset($stdout[count($stdout) - $i]))
echo $stdout[count($stdout) - $i] . PHP_EOL;
}
}
return 0;
}
}
{
}

View File

@@ -0,0 +1,35 @@
<?php
namespace ZM\Command\Server;
use Swoole\Process;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Daemon\DaemonCommand;
use ZM\Utils\DataProvider;
class ServerStopCommand extends DaemonCommand
{
protected static $defaultName = 'server:stop';
protected function configure() {
$this->setDescription("停止运行的框架");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
parent::execute($input, $output);
Process::kill(intval($this->daemon_file["pid"]), SIGTERM);
$i = 10;
while (file_exists(DataProvider::getWorkingDir() . "/.daemon_pid") && $i > 0) {
sleep(1);
--$i;
}
if ($i === 0) {
$output->writeln("<error>停止失败请检查进程pid #" . $this->daemon_file["pid"] . " 是否响应!</error>");
} else {
$output->writeln("<info>成功停止!</info>");
}
return 0;
}
}

View File

@@ -21,14 +21,17 @@ use ZM\Command\RunServerCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Command\Server\ServerReloadCommand;
use ZM\Command\Server\ServerStatusCommand;
use ZM\Command\Server\ServerStopCommand;
use ZM\Exception\InitException;
class ConsoleApplication extends Application
{
private static $obj = null;
const VERSION_ID = 425;
const VERSION = "2.5.7";
const VERSION_ID = 429;
const VERSION = "2.6.2";
/**
* @throws InitException
@@ -46,8 +49,8 @@ class ConsoleApplication extends Application
* @return ConsoleApplication
* @throws InitException
*/
public function initEnv($with_default_cmd = ""): ConsoleApplication {
if (defined("WORKDING_DIR")) throw new InitException();
public function initEnv(string $with_default_cmd = ""): ConsoleApplication {
if (defined("WORKING_DIR")) throw new InitException();
_zm_env_check();
@@ -72,7 +75,7 @@ class ConsoleApplication extends Application
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(WORKING_DIR . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) {
echo "成功添加!请运行 composer dump-autoload \n";
echo "成功添加!请运行 'composer dump-autoload'\n";
exit(0);
} else {
echo zm_internal_errcode("E00006") . "添加失败!请按任意键继续!";
@@ -90,6 +93,9 @@ class ConsoleApplication extends Application
new DaemonReloadCommand(),
new DaemonStopCommand(),
new RunServerCommand(), //运行主服务的指令控制器
new ServerStatusCommand(),
new ServerStopCommand(),
new ServerReloadCommand(),
new PureHttpCommand(), //纯HTTP服务器指令
new SystemdGenerateCommand()
]);

View File

@@ -9,6 +9,7 @@ use Swoole\Coroutine;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Console\Console;
@@ -19,6 +20,7 @@ use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response;
use ZM\API\ZMRobot;
use ZM\Utils\CoMessage;
use ZM\Utils\MessageUtil;
class Context implements ContextInterface
{
@@ -56,8 +58,8 @@ class Context implements ContextInterface
*/
public function getResponse(): ?Response { return self::$context[$this->cid]["response"] ?? null; }
/** @return ConnectionObject|null|Response */
public function getConnection() { return ManagerGM::get($this->getFd()); }
/** @return ConnectionObject|null */
public function getConnection(): ?ConnectionObject { return ManagerGM::get($this->getFd()); }
/**
* @return int|null
@@ -72,9 +74,20 @@ class Context implements ContextInterface
return $conn instanceof ConnectionObject ? new ZMRobot($conn) : null;
}
public function getMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; }
public function getMessage() {
if ((ZMConfig::get("global", "onebot")["message_convert_string"] ?? true) === true && is_array($msg = $this->getOriginMessage())) {
return MessageUtil::arrayToStr($msg);
} else {
return self::$context[$this->cid]["data"]["message"] ?? null;
}
}
public function setMessage($msg) { self::$context[$this->cid]["data"]["message"] = $msg; }
public function setMessage($msg) {
if (is_string($msg) && is_array($this->getOriginMessage())) {
$msg = MessageUtil::strToArray($msg);
}
self::$context[$this->cid]["data"]["message"] = $msg;
}
public function getUserId() { return $this->getData()["user_id"] ?? null; }
@@ -168,7 +181,11 @@ class Context implements ContextInterface
if ($r === false) {
throw new WaitTimeoutException($this, $timeout_prompt);
}
return $r["message"];
if (is_array($r["message"]) && (ZMConfig::get("global", "onebot")["message_convert_string"] ?? true) === true) {
return MessageUtil::arrayToStr($r["message"]);
} else {
return $r["message"];
}
}
/**
@@ -239,4 +256,18 @@ class Context implements ContextInterface
public function copy() { return self::$context[$this->cid]; }
public function getOption() { return self::getCache("match"); }
public function getOriginMessage() { return self::$context[$this->cid]["data"]["message"] ?? null; }
public function getArrayMessage(): array {
$msg = $this->getOriginMessage();
if (is_array($msg)) return $msg;
else return MessageUtil::strToArray($msg);
}
public function getStringMessage(): string {
$msg = $this->getOriginMessage();
if (is_string($msg)) return $msg;
else return MessageUtil::arrayToStr($msg);
}
}

View File

@@ -12,6 +12,7 @@ use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnTick;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Exception\AnnotationException;
use ZM\Store\LightCache;
use ZM\Store\ZMAtomic;
@@ -32,6 +33,9 @@ class EventManager
(new AnnotationParser())->sortByLevel(self::$events, $event_name);
}
/**
* @throws AnnotationException
*/
public static function loadEventByParser(AnnotationParser $parser) {
self::$events = $parser->generateAnnotationEvents();
self::$middlewares = $parser->getMiddlewares();

View File

@@ -7,6 +7,7 @@ namespace ZM\Event\SwooleEvent;
use Swoole\Process;
use Swoole\WebSocket\Server;
use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Event\SwooleEvent;
@@ -22,7 +23,7 @@ class OnBeforeReload implements SwooleEvent
for ($i = 0; $i < ZM_WORKER_NUM; ++$i) {
Process::kill(zm_atomic("_#worker_" . $i)->get(), SIGUSR1);
}
usleep(800 * 1000);
$conf = ZMConfig::get("global", "runtime")["reload_delay_time"] ?? 800;
if ($conf !== 0) usleep($conf * 1000);
}
}

View File

@@ -8,6 +8,7 @@ use Swoole\Server;
use ZM\Annotation\Swoole\SwooleHandler;
use ZM\Console\Console;
use ZM\Event\SwooleEvent;
use ZM\Utils\DataProvider;
/**
* Class OnShutdown
@@ -18,5 +19,9 @@ class OnShutdown implements SwooleEvent
{
public function onCall(Server $server) {
Console::verbose("正在关闭 Master 进程pid=" . posix_getpid());
$pid_path = DataProvider::getWorkingDir() . "/.daemon_pid";
if (file_exists($pid_path)) {
unlink($pid_path);
}
}
}

View File

@@ -25,13 +25,12 @@ class OnStart implements SwooleEvent
if (!Framework::$argv["disable-safe-exit"]) {
SignalListener::signalMaster($server);
}
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);
}
$daemon_data = json_encode([
"pid" => $server->master_pid,
"stdout" => ZMConfig::get("global")["swoole"]["log_file"],
"daemon" => (bool)Framework::$argv["daemon"]
], 128 | 256);
file_put_contents(DataProvider::getWorkingDir() . "/.daemon_pid", $daemon_data);
}

View File

@@ -4,6 +4,7 @@
namespace ZM\Module;
use Exception;
use ZM\Annotation\CQ\CQAfter;
use ZM\Annotation\CQ\CQAPIResponse;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQCommand;
@@ -11,6 +12,7 @@ use ZM\Annotation\CQ\CQMessage;
use ZM\Annotation\CQ\CQMetaEvent;
use ZM\Annotation\CQ\CQNotice;
use ZM\Annotation\CQ\CQRequest;
use ZM\Config\ZMConfig;
use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException;
use ZM\Exception\WaitTimeoutException;
@@ -59,9 +61,25 @@ class QQBot
}
if (isset($data["post_type"])) $this->dispatchEvents($data);
else $this->dispatchAPIResponse($data);
if (($data["post_type"] ?? "meta_event") != "meta_event") {
$r = $this->dispatchAfterEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (WaitTimeoutException $e) {
if (($data["post_type"] ?? "meta_event") != "meta_event") {
$r = $this->dispatchAfterEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
$e->module->finalReply($e->getMessage());
} catch (InterruptException $e) {
if (($data["post_type"] ?? "meta_event") != "meta_event") {
$r = $this->dispatchAfterEvents($data); // before在这里执行元事件不执行before为减少不必要的调试日志
if ($r->store === "block") EventDispatcher::interrupt();
}
throw $e;
}
// 这里修复CQAfter不能使用的问题我竟然一直没写绝了
}
/**
@@ -73,11 +91,11 @@ class QQBot
public function dispatchBeforeEvents($data, $time): EventDispatcher {
$before = new EventDispatcher(CQBefore::class);
if ($time === "pre") {
$before->setRuleFunction(function($v) use ($data){
$before->setRuleFunction(function ($v) use ($data) {
return $v->level >= 200 && $v->cq_event == $data["post_type"];
});
} else {
$before->setRuleFunction(function($v) use ($data){
$before->setRuleFunction(function ($v) use ($data) {
return $v->level < 200 && $v->cq_event == $data["post_type"];
});
}
@@ -103,22 +121,33 @@ class QQBot
if (is_string($result)) ctx()->reply($result);
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
});
$s = MessageUtil::matchCommand(ctx()->getMessage(), ctx()->getData());
$s = MessageUtil::matchCommand(ctx()->getStringMessage(), ctx()->getData());
if ($s->status !== false) {
if (!empty($s->match)) ctx()->setCache("match", $s->match);
$dispatcher->dispatchEvent($s->object, null);
if (is_string($dispatcher->store)) ctx()->reply($dispatcher->store);
if (ctx()->getCache("has_reply") === true) EventDispatcher::interrupt();
if (ctx()->getCache("has_reply") === true) {
$policy = ZMConfig::get("global", "onebot")['message_command_policy'] ?? 'interrupt';
switch ($policy) {
case 'interrupt':
EventDispatcher::interrupt();
break;
case 'continue':
break;
default:
throw new Exception("未知的消息命令策略:" . $policy);
}
}
}
//分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class);
$msg_dispatcher->setRuleFunction(function ($v) {
return ($v->message == '' || ($v->message != '' && $v->message == context()->getData()["message"])) &&
($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == context()->getData()["user_id"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == (context()->getData()["group_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == context()->getData()["message_type"])) &&
($v->raw_message == '' || ($v->raw_message != '' && $v->raw_message == context()->getData()["raw_message"]));
return ($v->message == '' || ($v->message == ctx()->getStringMessage())) &&
($v->user_id == 0 || ($v->user_id == ctx()->getUserId())) &&
($v->group_id == 0 || ($v->group_id == (ctx()->getGroupId() ?? 0))) &&
($v->message_type == '' || ($v->message_type == ctx()->getMessageType())) &&
($v->raw_message == '' || ($v->raw_message == context()->getData()["raw_message"]));
});
$msg_dispatcher->setReturnFunction(function ($result) {
if (is_string($result)) ctx()->reply($result);
@@ -158,6 +187,16 @@ class QQBot
}
}
private function dispatchAfterEvents($data): EventDispatcher {
$after = new EventDispatcher(CQAfter::class);
$after->setRuleFunction(function ($v) use ($data) {
return $v->cq_event == $data["post_type"];
});
zm_dump("开始触发!", $data);
$after->dispatchEvents($data);
return $after;
}
/**
* @param $req
* @throws Exception

View File

@@ -59,7 +59,7 @@ class MessageUtil
return false;
}
public static function isAtMe($msg, $me_id) {
public static function isAtMe($msg, $me_id): bool {
return strpos($msg, CQ::at($me_id)) !== false;
}
@@ -72,7 +72,7 @@ class MessageUtil
* @param int $type
* @return string
*/
public static function getImageCQFromLocal($file, $type = 0): string {
public static function getImageCQFromLocal($file, int $type = 0): string {
switch ($type) {
case 0:
return CQ::image("base64://" . base64_encode(file_get_contents($file)));
@@ -90,7 +90,7 @@ class MessageUtil
* @param $msg
* @return array|string[]
*/
public static function splitCommand($msg) {
public static function splitCommand($msg): array {
$word = explodeMsg(str_replace("\r", "", $msg));
if (empty($word)) $word = [""];
if (count(explode("\n", $word[0])) >= 2) {
@@ -98,7 +98,7 @@ class MessageUtil
$first = split_explode(" ", array_shift($enter));
$word = array_merge($first, $enter);
foreach ($word as $k => $v) {
$word[$k] = trim($word[$k]);
$word[$k] = trim($v);
}
}
return $word;
@@ -109,15 +109,18 @@ class MessageUtil
* @param $obj
* @return MatchResult
*/
public static function matchCommand($msg, $obj) {
public static function matchCommand($msg, $obj): MatchResult {
$ls = EventManager::$events[CQCommand::class] ?? [];
if (is_array($msg)) {
$msg = self::arrayToStr($msg);
}
$word = self::splitCommand($msg);
$matched = new MatchResult();
foreach ($ls as $v) {
if (array_diff([$v->match, $v->pattern, $v->regex, $v->keyword, $v->end_with, $v->start_with], [""]) == []) continue;
elseif (($v->user_id == 0 || ($v->user_id != 0 && $v->user_id == $obj["user_id"])) &&
($v->group_id == 0 || ($v->group_id != 0 && $v->group_id == ($obj["group_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type != '' && $v->message_type == $obj["message_type"]))
elseif (($v->user_id == 0 || ($v->user_id == $obj["user_id"])) &&
($v->group_id == 0 || ($v->group_id == ($obj["group_id"] ?? 0))) &&
($v->message_type == '' || ($v->message_type == $obj["message_type"]))
) {
if (($word[0] != "" && $v->match == $word[0]) || in_array($word[0], $v->alias)) {
array_shift($word);
@@ -166,4 +169,61 @@ class MessageUtil
ProcessManager::sendActionToWorker($i, "add_short_command", [$command, $reply]);
}
}
/**
* 字符串转数组
* @param $msg
* @param bool $ignore_space
* @param false $trim_text
* @return array
*/
public static function strToArray($msg, bool $ignore_space = true, bool $trim_text = false): array {
$arr = [];
while (($rear = mb_strstr($msg, '[CQ:')) !== false && ($end = mb_strstr($rear, ']', true)) !== false) {
// 把 [CQ: 前面的文字生成段落
$front = mb_strstr($msg, '[CQ:', true);
// 如果去掉空格都还有文字,或者不去掉空格有字符,且不忽略空格,则生成段落,否则不生成
if (($trim_front = trim($front)) !== '' || ($front !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_front : $front)]];
}
// 处理 CQ 码
$content = mb_substr($end, 4);
$cq = explode(",", $content);
$object_type = array_shift($cq);
$object_params = [];
foreach ($cq as $v) {
$key = mb_strstr($v, "=", true);
$object_params[$key] = CQ::decode(mb_substr(mb_strstr($v, "="), 1), true);
}
$arr[] = ["type" => $object_type, "data" => $object_params];
$msg = mb_substr(mb_strstr($rear, ']'), 1);
}
if (($trim_msg = trim($msg)) !== '' || ($msg !== '' && !$ignore_space)) {
$arr[] = ['type' => 'text', 'data' => ['text' => CQ::decode($trim_text ? $trim_msg : $msg)]];
}
return $arr;
}
/**
* 数组转字符串
* 纪念一下这段代码完全由AI生成没有人知道它是怎么写的这句话是我自己写的不知道是不是有人知道的
* @param array $array
* @return string
* @author Copilot
*/
public static function arrayToStr(array $array): string {
$str = "";
foreach ($array as $v) {
if ($v['type'] == 'text') {
$str .= $v['data']['text'];
} else {
$str .= "[CQ:" . $v['type'];
foreach ($v['data'] as $key => $value) {
$str .= "," . $key . "=" . CQ::encode($value, true);
}
$str .= "]";
}
}
return $str;
}
}

0
zhamao Executable file → Normal file
View File