mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-06 00:05:36 +08:00
Compare commits
29 Commits
3.0.0-beta
...
3.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e89d5ad1ac | ||
|
|
11aed8c6be | ||
|
|
46d2f895e2 | ||
|
|
ed6b65eb88 | ||
|
|
8efb63a334 | ||
|
|
13b5c44627 | ||
|
|
2a3c953c36 | ||
|
|
fd8b3721ae | ||
|
|
3734f5d476 | ||
|
|
088f963ad1 | ||
|
|
c8a6bc69d3 | ||
|
|
8584cc647c | ||
|
|
d8ac604592 | ||
|
|
9acb5e760b | ||
|
|
96879bf415 | ||
|
|
d7e815d670 | ||
|
|
dfcb8a4550 | ||
|
|
96fa6b105c | ||
|
|
b86f51ab46 | ||
|
|
8473a1152d | ||
|
|
65acfaa0bd | ||
|
|
c58e08998a | ||
|
|
9cf905421e | ||
|
|
c65694402f | ||
|
|
0d24ae6192 | ||
|
|
383e0e22af | ||
|
|
87840930e0 | ||
|
|
05b3321af7 | ||
|
|
19e380d1fb |
8
.github/workflows/vuepress-deploy.yml
vendored
8
.github/workflows/vuepress-deploy.yml
vendored
@@ -41,3 +41,11 @@ jobs:
|
||||
ssh_private_key: ${{ secrets.ZHAMAO_XIN_PRIVATE_KEY }}
|
||||
local_path: './ext/go.sh'
|
||||
remote_path: ${{ secrets.ZHAMAO_XIN_MAIN_TARGET }}
|
||||
- name: deploy script file
|
||||
uses: wlixcc/SFTP-Deploy-Action@v1.2
|
||||
with:
|
||||
username: ${{ secrets.ZHAMAO_XIN_USER }}
|
||||
server: ${{ secrets.ZHAMAO_XIN_HOST }}
|
||||
ssh_private_key: ${{ secrets.ZHAMAO_XIN_PRIVATE_KEY }}
|
||||
local_path: './ext/v3.sh'
|
||||
remote_path: ${{ secrets.ZHAMAO_XIN_MAIN_TARGET }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,7 +9,7 @@
|
||||
/site/
|
||||
/plugins/
|
||||
/doxy/
|
||||
|
||||
/walle/
|
||||
# 框架审计文件
|
||||
audit.log
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use ZM\Logger\ConsoleLogger;
|
||||
use ZM\Store\MockAtomic;
|
||||
|
||||
// 引入自动加载
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
require $_composer_autoload_path ?? dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
// 声明一个全局的原子计数,用于保存整个进程的退出状态码
|
||||
global $_swoole_atomic;
|
||||
|
||||
27
bin/zhamao.bat
Normal file
27
bin/zhamao.bat
Normal file
@@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
@REM Check if ZM_CUSTOM_PHP_PATH is set
|
||||
IF /i "%ZM_CUSTOM_PHP_PATH%" neq "" (
|
||||
@REM Set the path to the custom PHP
|
||||
echo "* Using custom PHP executable: %ZM_CUSTOM_PHP_PATH%"
|
||||
SET executable=%ZM_CUSTOM_PHP_PATH%
|
||||
) ELSE IF exist ./runtime/php.exe (
|
||||
@REM Set the path to the built-in PHP
|
||||
echo "* Using built-in PHP executable"
|
||||
SET executable=.\runtime\php.exe
|
||||
) ELSE (
|
||||
@REM Set the path to the system PHP
|
||||
echo "* Using system PHP executable"
|
||||
SET executable=php
|
||||
)
|
||||
@REM TODO: Phar write support is missing
|
||||
IF exist src/entry.php (
|
||||
@REM Run the PHP entry point
|
||||
%executable% src/entry.php %*
|
||||
) ELSE IF exist vendor/zhamao/framework/src/entry.php (
|
||||
@REM Run the PHP entry point
|
||||
%executable% vendor/zhamao/framework/src/entry.php %*
|
||||
) ELSE (
|
||||
@REM No entry point found
|
||||
echo "[ErrCode:E00015] Cannot find zhamao-framework entry file!"
|
||||
exit /b 1
|
||||
)
|
||||
@@ -73,7 +73,6 @@
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/gendoc",
|
||||
"bin/phpunit-zm",
|
||||
"bin/zhamao"
|
||||
],
|
||||
|
||||
@@ -18,12 +18,6 @@ $config['servers'] = [
|
||||
'type' => 'http',
|
||||
'flag' => 20002,
|
||||
],
|
||||
[
|
||||
'host' => '0.0.0.0',
|
||||
'port' => 20003,
|
||||
'type' => 'http',
|
||||
'flag' => 20003,
|
||||
],
|
||||
];
|
||||
|
||||
/* Workerman 驱动相关配置 */
|
||||
@@ -46,7 +40,7 @@ $config['swoole_options'] = [
|
||||
];
|
||||
|
||||
/* 默认存取炸毛数据的目录(相对目录时,代表WORKING_DIR下的目录,绝对目录按照绝对目录来) */
|
||||
$config['data_dir'] = 'zm_data';
|
||||
$config['data_dir'] = WORKING_DIR . '/zm_data';
|
||||
|
||||
/* 框架本体运行时的一些可调配置 */
|
||||
$config['runtime'] = [
|
||||
@@ -73,6 +67,7 @@ $config['plugin'] = [
|
||||
$config['native_plugin'] = [
|
||||
'onebot12' => true, // OneBot v12 协议支持
|
||||
'onebot12-ban-other-ws' => true, // OneBot v12 协议支持,禁止其他 WebSocket 连接
|
||||
'command-manual' => true,
|
||||
];
|
||||
|
||||
/* 静态文件读取器 */
|
||||
@@ -107,4 +102,11 @@ $config['database'] = [
|
||||
],
|
||||
];
|
||||
|
||||
/* KV 数据库的配置 */
|
||||
$config['kv'] = [
|
||||
'use' => \LightCache::class, // 默认在单进程模式下使用 LightCache,多进程需要使用 ZMRedis
|
||||
'light_cache_dir' => $config['data_dir'] . '/lc', // 默认的 LightCache 保存持久化数据的位置
|
||||
'light_cache_autosave_time' => 600, // LightCache 自动保存时间(秒)
|
||||
];
|
||||
|
||||
return $config;
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = {
|
||||
activeHeaderLinks: false,
|
||||
nav: [
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: '事件', link: '/event/' },
|
||||
{ text: 'API 文档', link: '/doxy/', target: '_blank' },
|
||||
{ text: '炸毛框架 v2', link: 'https://docs-v2.zhamao.xin/' }
|
||||
],
|
||||
@@ -44,6 +45,21 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
],
|
||||
'/event/': [
|
||||
{
|
||||
title: '事件',
|
||||
collapsable: false,
|
||||
sidebarDepth: 1,
|
||||
children: [
|
||||
'',
|
||||
'bot',
|
||||
'http',
|
||||
'middleware',
|
||||
'framework',
|
||||
'extend',
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
docs/event/README.md
Normal file
25
docs/event/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 入门
|
||||
|
||||
## 事件是什么
|
||||
|
||||
简单来说,事件是一个底层的 Event Loop 收到消息后调用对应的方法的一个模型,比如给机器人发送消息后框架会调用你定义的方法来执行你的业务代码。
|
||||
|
||||
## 属性和注解是什么
|
||||
|
||||
属性(Attribute)是 PHP 8 最大的新变化之一,是 PHP 官方支持的、内置的注解实现,允许我们通过编程方式获取对应的元数据,可以大大方便我们对某一类代码进行处理。
|
||||
|
||||
而注解(Annotation)则是在 PHP 尚未支持属性的时代,用来代替的社区实现方案,通过解析 PHPDoc 注释来实现自己的注解机制。
|
||||
|
||||
炸毛框架同时支持注解和属性,在文档当中,有时会混用两者的字眼,在大多数情况下都可以安全地交换使用,例如 `#[BotEvent]` 和 `@BotEvent` 的行为是完全一致的。
|
||||
|
||||
## 注解和事件的关系
|
||||
|
||||
在炸毛框架中,注解是事件分发的一个重要角色,但注解本身并非事件,更恰当地说,注解代表了事件。
|
||||
|
||||
无论是机器人开发过程中场景的 `#[BotCommand]` 或是 HTTP 服务的路由 `#[RequestMapping]` 都是注解代表事件的例子。
|
||||
|
||||
## 阻断事件分发
|
||||
|
||||
在炸毛框架中,事件由一个统一的事件分发器进行分发,你可以在任意事件中阻断所有后续的分发。
|
||||
|
||||
(待考)
|
||||
86
docs/event/bot.md
Normal file
86
docs/event/bot.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 机器人事件
|
||||
|
||||
<aside>
|
||||
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\OneBot`
|
||||
|
||||
</aside>
|
||||
|
||||
> 在使用注解绑定事件时,如果不存在 **必需** 参数,可一个参数都不写,效果就是此事件在任何情况下都会调用此方法,例如 `#[BotEvent()]` 会在收到任意机器人事件时调用。
|
||||
>
|
||||
|
||||
## BotAction
|
||||
|
||||
啊?
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| action | string | 动作名称 | “” |
|
||||
| need_response | string | 动作是否需要响应 | false |
|
||||
| level | int | 事件优先级(越大越先执行) | 20 |
|
||||
|
||||
## BotActionResponse
|
||||
|
||||
啊??
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| retcode | int | 响应码 | null |
|
||||
| level | int | 事件优先级(越大越先执行) | 20 |
|
||||
|
||||
## BotEvent
|
||||
|
||||
用于处理所有的机器人事件,具体的参数含义可以参见 [https://12.onebot.dev/connect/data-protocol/event/](https://12.onebot.dev/connect/data-protocol/event/)。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| type | string | 对应标准中的事件类型 | null |
|
||||
| detail_type | string | 对应标准中的事件详细类型 | null |
|
||||
| sub_type | string | 对应标准中的事件子类型 | null |
|
||||
| level | int | 事件优先级(越大越先执行) | 20 |
|
||||
|
||||
## BotCommand
|
||||
|
||||
对于 `BotEvent` 的封装,用于支持常用的命令式调用(如:”天气 深圳”)。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | string | 命令名称,应全局唯一 | “” |
|
||||
| match | string | 匹配第一个词的命令式消息,如 天气 北京 中的 天气 | “” |
|
||||
| pattern | string | 根据 * 号通配符进行模式匹配用户消息,如 查询*天气 | “” |
|
||||
| regex | string 合法的正则表达式 | 匹配正则表达式匹配到的用户消息 | “” |
|
||||
| start_with | string | 匹配消息开头相匹配的消息,如 我叫炸毛,这里写 我叫 | “” |
|
||||
| end_with | string | 匹配消息结尾相匹配的消息,以 start_with 类推 | “” |
|
||||
| keyword | string | 匹配消息中有相关关键词的消息 | “” |
|
||||
| alias | string | match 匹配到命令的别名,数组形式 | [] |
|
||||
| detail_type | string | 限定消息事件的详细类型,见 BotEvent | “” |
|
||||
| prefix | string | | |
|
||||
| level | int | 事件优先级(越大越先执行) | 20 |
|
||||
|
||||
> 机器人命令注册的实例可参见【一堆例子链接】
|
||||
>
|
||||
|
||||
## CommandArgument
|
||||
|
||||
与 BotCommand 搭配使用,可以自动识别参数及生成帮助。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | string | 参数名称 | “” |
|
||||
| description | string | 参数描述 | “” |
|
||||
| required | bool | 参数是否必需 | false |
|
||||
| prompt | string | 当参数缺失时请求用户输入时提示的消息(需要参数设为必需) | “请输入{$name}” |
|
||||
| default | mixed | 当参数非必需且未填入时的默认值 | null |
|
||||
| timeout | int 单位秒 | 请求输入超时时间 | 60 |
|
||||
|
||||
## CommandHelp
|
||||
|
||||
与 BotCommand 搭配使用,可以补充生成的帮助信息。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| description | string | 命令描述 | “” |
|
||||
| usage | string | 命令用法 | “” |
|
||||
| example | string | 命令示例 | “” |
|
||||
|
||||
> 关于自动帮助生成的更多信息,请参见 【这里链接】
|
||||
>
|
||||
3
docs/event/extend.md
Normal file
3
docs/event/extend.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 扩展事件分发器
|
||||
|
||||
TODO
|
||||
31
docs/event/framework.md
Normal file
31
docs/event/framework.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# 框架事件
|
||||
|
||||
<aside>
|
||||
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\Framework`
|
||||
|
||||
</aside>
|
||||
|
||||
## BindEvent
|
||||
|
||||
相对底层的事件绑定,支持绑定所有透过框架分发的事件。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| event_class | string | 时间名 | 必填 |
|
||||
| level | int | 事件优先级(越大越先执行) | 800 |
|
||||
|
||||
## Init
|
||||
|
||||
在 Worker 进程初始化时触发,用于进行 Worker 初始化。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| worker | int 由 0 至 (最大Worker数-1) | 限定执行的 Worker 进程,-1 为在所有 Worker 执行 | 0 |
|
||||
|
||||
## Setup
|
||||
|
||||
在框架初始化时触发,在主进程执行,不可使用协程相关功能。
|
||||
|
||||
可用于改变所有进程的设置,相关更改会随着进程创建应用到所有 Worker 和 Manager 进程。
|
||||
|
||||
*没有参数*
|
||||
25
docs/event/http.md
Normal file
25
docs/event/http.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 路由事件
|
||||
|
||||
<aside>
|
||||
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\Http`
|
||||
|
||||
</aside>
|
||||
|
||||
## Controller
|
||||
|
||||
对同一类下的路由进行修饰,只可在类上使用。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| prefix | string | 路由前缀,应用到类下的所有路由 | 必填 |
|
||||
|
||||
## Route
|
||||
|
||||
路由事件,当对应的路由收到请求时触发。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| route | string | 路由 | 必填 |
|
||||
| name | string | 路由名称 | “” |
|
||||
| request_method | array<string> | 允许的请求方法 | [’GET’, ‘POST’] |
|
||||
| params | array<string, string> | 路由参数 | [] |
|
||||
18
docs/event/middleware.md
Normal file
18
docs/event/middleware.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 中间件事件
|
||||
|
||||
<aside>
|
||||
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\Middleware`
|
||||
|
||||
</aside>
|
||||
|
||||
## Middleware
|
||||
|
||||
当绑定了此中间件的方法被触发时触发。
|
||||
|
||||
| 参数名称 | 允许值 | 用途 | 默认 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | string | 中间件名称 | 必填 |
|
||||
| params | array<mixed> | 中间件参数 | [] |
|
||||
|
||||
> 关于中间件的具体用法,请参见【再来链接】
|
||||
>
|
||||
271
ext/v3.sh
Executable file
271
ext/v3.sh
Executable file
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 支持的环境变量:
|
||||
# ZM_DOWN_PHP_VERSION php版本,默认为8.0
|
||||
# ZM_NO_LOCAL_PHP 如果填入任意内容,则不检查本地PHP,直接安装内建PHP(仅限Linux)
|
||||
# ZM_TEMP_DIR 脚本下载内建PHP和Composer的临时目录,默认为/tmp/.zm-runtime
|
||||
# ZM_CUSTOM_DIR 自定义新建目录名称,默认为zhamao-v3
|
||||
# ZM_COMPOSER_PACKAGIST 是否使用Composer的国外源,默认使用国内阿里云,如果设置并填写了内容,则自动使用Composer的国外源
|
||||
|
||||
ZM_PWD=$(pwd)
|
||||
_cyan="\033[0;36m"
|
||||
_reset="\033[0m"
|
||||
|
||||
# 彩头
|
||||
function nhead() {
|
||||
if [ "$1" = "red" ]; then
|
||||
echo -ne "\033[0;31m[!]"
|
||||
elif [ "$1" = "yellow" ]; then
|
||||
echo -ne "\033[0;33m[?]"
|
||||
else
|
||||
echo -ne "\033[0;32m[*]"
|
||||
fi
|
||||
echo -e "\033[0m"
|
||||
}
|
||||
|
||||
# 下载文件 $1 到目录 $2,自动选择使用 curl 或者 wget
|
||||
function download_file() {
|
||||
downloader="wget"
|
||||
type wget >/dev/null 2>&1 || { downloader="curl"; }
|
||||
if [ "$downloader" = "wget" ]; then
|
||||
_down_prefix="O"
|
||||
else
|
||||
_down_prefix="o"
|
||||
fi
|
||||
_down_symbol=0
|
||||
if [ ! -f "$2" ]; then
|
||||
echo -ne "$(nhead) 正在下载 $1 ... "
|
||||
$downloader "$1" -$_down_prefix "$2" >/dev/null 2>&1 && echo "完成!" && _down_symbol=1
|
||||
else
|
||||
echo "已存在!" && _down_symbol=1
|
||||
fi
|
||||
if [ $_down_symbol == 0 ]; then
|
||||
echo "$(nhead red) 下载失败!请检查网络连接!"
|
||||
rm -rf "$2"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# 安装下载内建PHP
|
||||
function install_native_php() {
|
||||
ZM_PHP_VERSION="8.1"
|
||||
if [ "$ZM_DOWN_PHP_VERSION" != "" ]; then
|
||||
ZM_PHP_VERSION="$ZM_DOWN_PHP_VERSION"
|
||||
fi
|
||||
echo "$(nhead) 使用的内建 PHP 版本: $ZM_PHP_VERSION"
|
||||
|
||||
rm -rf "$ZM_TEMP_DIR"
|
||||
mkdir "$ZM_TEMP_DIR" >/dev/null 2>&1
|
||||
if [ ! -f "$ZM_TEMP_DIR/php" ]; then
|
||||
download_file "https://dl.zhamao.xin/php-bin/down.php?php_ver=$ZM_PHP_VERSION&arch=$(uname -m)" "$ZM_TEMP_DIR/php.tgz" || return 1
|
||||
tar -xf "$ZM_TEMP_DIR/php.tgz" -C "$ZM_TEMP_DIR/" && rm -rf "$ZM_TEMP_DIR/php.tgz"
|
||||
fi
|
||||
echo "$(nhead) 安装内建 PHP 完成!"
|
||||
php_executable="$ZM_TEMP_DIR/php"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 安装下载Composer
|
||||
function install_native_composer() {
|
||||
if [ ! -f "$ZM_TEMP_DIR/composer.phar" ]; then
|
||||
# 下载 composer.phar
|
||||
download_file "https://mirrors.aliyun.com/composer/composer.phar" "$ZM_TEMP_DIR/composer.phar" || return 1
|
||||
if [ "$php_executable" = "$ZM_TEMP_DIR/php" ]; then
|
||||
# shellcheck disable=SC2016
|
||||
txt='#!/usr/bin/env sh
|
||||
if [ -f "$(dirname $0)/php" ]; then
|
||||
"$(dirname $0)/php" "$(dirname $0)/composer.phar" $*
|
||||
else
|
||||
php "$(dirname $0)/composer.phar" $*
|
||||
fi'
|
||||
echo "$txt" >"$ZM_TEMP_DIR/composer"
|
||||
chmod +x "$ZM_TEMP_DIR/composer"
|
||||
else
|
||||
mv "$ZM_TEMP_DIR/composer.phar" "$ZM_TEMP_DIR/composer" && chmod +x "$ZM_TEMP_DIR/composer"
|
||||
fi
|
||||
fi
|
||||
echo "$(nhead) 安装内建 Composer 完成!"
|
||||
composer_executable="$ZM_TEMP_DIR/composer"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 检查Composer可用性
|
||||
function composer_check() {
|
||||
# 顺带检查一下
|
||||
echo "$(nhead) 正在检查 Git、unzip、7z 能否正常使用 ... "
|
||||
type git >/dev/null 2>&1 || {
|
||||
echo "$(nhead red) 检测到系统不存在 git 命令,可能无法正常使用 Composer 下载 GitHub 等仓库项目!"
|
||||
}
|
||||
zip_check_symbol=0
|
||||
if type unzip >/dev/null 2>&1; then
|
||||
zip_check_symbol=1
|
||||
fi
|
||||
if type 7z >/dev/null 2>&1; then
|
||||
zip_check_symbol=1
|
||||
fi
|
||||
if [ $zip_check_symbol -eq 0 ]; then
|
||||
if [ "$($php_executable -m | grep zip)" = "" ]; then
|
||||
echo "$(nhead red) 检测到系统不存在 unzip 或 7z 命令,PHP 不存在 zip 扩展,可能无法正常使用 Composer 下载压缩包项目!"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 测试 Composer 和 PHP 是否能正常使用
|
||||
if [ "$("$composer_executable" -n about | grep Manager)" = "" ]; then
|
||||
echo "$(nhead red) Download PHP binary and composer failed!"
|
||||
return 1
|
||||
fi
|
||||
echo "$(nhead) 环境检查完成!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# 环境检查
|
||||
function darwin_env_check() {
|
||||
echo -ne "$(nhead) 检查是否存在 PHP ... "
|
||||
if type php >/dev/null 2>&1; then
|
||||
php_executable=$(which php)
|
||||
echo "位置:$php_executable"
|
||||
if [ "$($php_executable -m | grep swoole)" = "" ]; then
|
||||
echo "$(nhead red) PHP 不存在 swoole 扩展,可能无法正常使用 Swoole 框架!" && return 1
|
||||
fi
|
||||
else
|
||||
echo "不存在"
|
||||
if type brew >/dev/null 2>&1; then
|
||||
echo -n "$(nhead yellow) 是否使用 Homebrew 安装 PHP?[y/N] "
|
||||
read -r y
|
||||
if [ "$y" = "" ]; then y="N"; fi
|
||||
if [ "$y" = "y" ]; then
|
||||
brew install php || echo "$(nhead red) 安装 PHP 失败!" && return 1
|
||||
else
|
||||
echo "$(nhead red) 跳过安装 PHP!" && return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -ne "$(nhead) 检查是否存在 Composer ... "
|
||||
if type composer >/dev/null 2>&1; then
|
||||
composer_executable=$(which composer)
|
||||
echo "位置:$composer_executable"
|
||||
else
|
||||
echo "不存在,正在下载 Composer ..."
|
||||
install_native_composer || return 1
|
||||
fi
|
||||
|
||||
composer_check || return 1
|
||||
}
|
||||
|
||||
# 询问是否安装 native php
|
||||
function prompt_install_native_php() {
|
||||
echo -ne "$(nhead yellow) 检测到系统的 PHP 不存在 swoole 扩展,是否下载安装独立的内建 PHP 和 Composer?[Y/n] "
|
||||
read -r y
|
||||
case $y in
|
||||
Y|y|"") return 0 ;;
|
||||
*) echo "$(nhead red) 跳过安装内建 PHP!" && return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 环境检查
|
||||
function linux_env_check() {
|
||||
if [ "$ZM_NO_LOCAL_PHP" != "" ]; then # 如果指定了不使用本地 php,则不检查,直接下载
|
||||
install_native_php && install_native_composer && composer_check && return 0 || return 1
|
||||
else
|
||||
echo -ne "$(nhead) 检查是否存在 PHP ... "
|
||||
if type php >/dev/null 2>&1; then
|
||||
php_executable=$(which php)
|
||||
echo "位置:$php_executable"
|
||||
if [ "$($php_executable -m | grep swoole)" = "" ]; then
|
||||
echo "$(nhead red) PHP 不存在 swoole 扩展,可能无法正常使用 Swoole 框架!" && \
|
||||
prompt_install_native_php && \
|
||||
install_native_php && \
|
||||
install_native_composer && composer_check && return 0 || return 1
|
||||
fi
|
||||
else
|
||||
echo "不存在,将下载内建 PHP"
|
||||
install_native_php || return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -ne "$(nhead) 检查是否存在 Composer ... "
|
||||
if type composer >/dev/null 2>&1; then
|
||||
composer_executable=$(which composer)
|
||||
echo "位置:$composer_executable"
|
||||
else
|
||||
echo "不存在,将下载内建 Composer"
|
||||
install_native_composer || return 1
|
||||
fi
|
||||
|
||||
composer_check || return 1
|
||||
}
|
||||
|
||||
function if_use_aliyun() {
|
||||
if [ "$ZM_COMPOSER_PACKAGIST" = "" ]; then
|
||||
$composer_executable -n config repos.packagist composer https://mirrors.aliyun.com/composer
|
||||
fi
|
||||
}
|
||||
|
||||
function if_restore_native_runtime() {
|
||||
ZM_RUNTIME_DIR="$ZM_PWD/$ZM_CUSTOM_DIR/runtime/"
|
||||
if [ "$php_executable" = "$ZM_TEMP_DIR/php" ]; then
|
||||
echo "$(nhead) 移动内建 PHP 到框架目录 $ZM_RUNTIME_DIR ..." && \
|
||||
mkdir -p "$ZM_RUNTIME_DIR" && \
|
||||
mv "$ZM_TEMP_DIR/php" "$ZM_RUNTIME_DIR" || {
|
||||
echo "$(nhead red) 移动内建 PHP 到框架目录失败!" && return 1
|
||||
}
|
||||
php_executable="$ZM_RUNTIME_DIR/php"
|
||||
fi
|
||||
if [ "$composer_executable" = "$ZM_TEMP_DIR/composer" ]; then
|
||||
echo "$(nhead) 移动内建 Composer 到框架目录 $ZM_RUNTIME_DIR ..." && \
|
||||
mkdir -p "$ZM_CUSTOM_DIR/runtime" && \
|
||||
mv "$ZM_TEMP_DIR/composer" "$ZM_RUNTIME_DIR" && \
|
||||
mv "$ZM_TEMP_DIR/composer.phar" "$ZM_RUNTIME_DIR" || {
|
||||
echo "$(nhead red) 移动内建 Composer 到框架目录失败!" && return 1
|
||||
}
|
||||
composer_executable="$ZM_RUNTIME_DIR/composer"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
function install_framework() {
|
||||
echo "$(nhead) 开始安装框架到目录 $ZM_CUSTOM_DIR ..."
|
||||
export COMPOSER_ALLOW_SUPERUSER=1
|
||||
mkdir -p "$ZM_PWD/$ZM_CUSTOM_DIR" && \
|
||||
cd "$ZM_PWD/$ZM_CUSTOM_DIR" && \
|
||||
$composer_executable init --name="zhamao/zhamao-v3-app" -n -q && \
|
||||
if_use_aliyun && \
|
||||
echo "$(nhead) 从 Composer 拉取框架 ..." && \
|
||||
echo '{"minimum-stability":"dev"}' > composer.json && composer require -n -q zhamao/framework:^3 && \
|
||||
$composer_executable require -n -q --dev swoole/ide-helper:^4.5 && \
|
||||
if_restore_native_runtime && \
|
||||
echo "$(nhead) 初始化框架脚手架文件 ..." && \
|
||||
vendor/bin/zhamao init >/dev/null 2>&1 && \
|
||||
$composer_executable dump-autoload -n -q && \
|
||||
show_success_msg || {
|
||||
echo "$(nhead red) 安装框架失败!" && cd $ZM_PWD && rm -rf "$ZM_CUSTOM_DIR" && return 1
|
||||
}
|
||||
}
|
||||
|
||||
function show_success_msg() {
|
||||
echo -e "$(nhead) 框架安装成功,已安装到目录 $ZM_CUSTOM_DIR" && \
|
||||
echo -e "$(nhead) 进入应用目录:""$_cyan""cd $ZM_CUSTOM_DIR""$_reset" && \
|
||||
echo -e "$(nhead) 启动框架命令:""$_cyan""./zhamao server""$_reset" && \
|
||||
echo -e "$(nhead) 生成插件脚手架目录的命令:""$_cyan""./zhamao plugin:make""$_reset"
|
||||
}
|
||||
|
||||
# 环境变量设置
|
||||
test "$ZM_TEMP_DIR" = "" && ZM_TEMP_DIR="/tmp/.zm-runtime"
|
||||
test "$ZM_CUSTOM_DIR" = "" && ZM_CUSTOM_DIR="zhamao-v3"
|
||||
|
||||
if [ -d "$ZM_PWD/$ZM_CUSTOM_DIR" ]; then
|
||||
echo "$(nhead red) 检测到目录 $ZM_CUSTOM_DIR/ 已安装过框架,请更换文件夹名称或删除旧文件夹再试!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查系统环境,目前只支持 Linux
|
||||
case $(uname -s) in
|
||||
Linux) linux_env_check || exit 1 ;;
|
||||
Darwin) darwin_env_check || exit 1 ;;
|
||||
*) echo "$(nhead red) Only support Linux and macOS!" && exit 1 ;;
|
||||
esac
|
||||
|
||||
# 安装框架
|
||||
install_framework
|
||||
@@ -4,6 +4,8 @@ parameters:
|
||||
paths:
|
||||
- ./src/
|
||||
- ./tests/
|
||||
excludePaths:
|
||||
- ./src/ZM/Exception/Solution/SolutionRepository.php
|
||||
ignoreErrors:
|
||||
- '#Constant .* not found#'
|
||||
- '#PHPDoc tag @throws with type Psr\\Container\\ContainerExceptionInterface is not subtype of Throwable#'
|
||||
|
||||
@@ -15,6 +15,7 @@ class_alias(\ZM\Annotation\Closed::class, 'Closed');
|
||||
class_alias(\ZM\Plugin\ZMPlugin::class, 'ZMPlugin');
|
||||
class_alias(\OneBot\V12\Object\OneBotEvent::class, 'OneBotEvent');
|
||||
class_alias(\ZM\Context\BotContext::class, 'BotContext');
|
||||
class_alias(\ZM\Store\KV\LightCache::class, 'LightCache');
|
||||
|
||||
// 下面是 OneBot 相关类的全局别称
|
||||
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');
|
||||
|
||||
@@ -6,14 +6,15 @@ use OneBot\Driver\Coroutine\Adaptive;
|
||||
use OneBot\Driver\Coroutine\CoroutineInterface;
|
||||
use OneBot\Driver\Process\ExecutionResult;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use OneBot\V12\Object\OneBotEvent;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Config\ZMConfig;
|
||||
use ZM\Container\ContainerHolder;
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
use ZM\Middleware\MiddlewareHandler;
|
||||
use ZM\Store\Database\DBException;
|
||||
use ZM\Store\Database\DBQueryBuilder;
|
||||
use ZM\Store\Database\DBWrapper;
|
||||
use ZM\Store\KV\KVInterface;
|
||||
|
||||
// 防止重复引用引发报错
|
||||
if (function_exists('zm_internal_errcode')) {
|
||||
@@ -210,7 +211,7 @@ function db(string $name = '')
|
||||
*
|
||||
* @throws DBException
|
||||
*/
|
||||
function sql_builder(string $name = '')
|
||||
function sql_builder(string $name = ''): DBQueryBuilder
|
||||
{
|
||||
return (new DBWrapper($name))->createQueryBuilder();
|
||||
}
|
||||
@@ -241,10 +242,17 @@ function config(array|string $key = null, mixed $default = null)
|
||||
|
||||
function bot(): ZM\Context\BotContext
|
||||
{
|
||||
if (\container()->has('bot.event')) {
|
||||
/** @var OneBotEvent $bot_event */
|
||||
$bot_event = \container()->get('bot.event');
|
||||
return new \ZM\Context\BotContext($bot_event->self['user_id'] ?? '', $bot_event->self['platform']);
|
||||
if (container()->has(ZM\Context\BotContext::class)) {
|
||||
return container()->get(ZM\Context\BotContext::class);
|
||||
}
|
||||
return new \ZM\Context\BotContext('', '');
|
||||
}
|
||||
|
||||
function kv(string $name = ''): KVInterface
|
||||
{
|
||||
global $kv_class;
|
||||
if (!$kv_class) {
|
||||
$kv_class = config('global.kv.use', \LightCache::class);
|
||||
}
|
||||
return $kv_class::open($name);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ namespace Module\Example;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||||
use ZM\Annotation\Http\Route;
|
||||
use ZM\Annotation\Middleware\Middleware;
|
||||
use ZM\Annotation\OneBot\BotCommand;
|
||||
use ZM\Annotation\OneBot\BotEvent;
|
||||
use ZM\Annotation\OneBot\CommandArgument;
|
||||
use ZM\Annotation\OneBot\CommandHelp;
|
||||
use ZM\Context\BotContext;
|
||||
use ZM\Middleware\TimerMiddleware;
|
||||
|
||||
class Hello123
|
||||
@@ -24,4 +28,12 @@ class Hello123
|
||||
{
|
||||
logger()->info("收到了 {$event->getType()}.{$event->getDetailType()} 事件");
|
||||
}
|
||||
|
||||
#[BotCommand('echo', 'echo')]
|
||||
#[CommandArgument('text', '要回复的内容', required: true)]
|
||||
#[CommandHelp('复读机', '只需要发送 echo+内容 即可自动复读', 'echo 你好 会回复 你好')]
|
||||
public function repeat(\OneBotEvent $event, BotContext $context): void
|
||||
{
|
||||
$context->reply($event->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
35
src/ZM/Annotation/OneBot/CommandHelp.php
Normal file
35
src/ZM/Annotation/OneBot/CommandHelp.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Annotation\OneBot;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
|
||||
use Doctrine\Common\Annotations\Annotation\Target;
|
||||
use ZM\Annotation\AnnotationBase;
|
||||
|
||||
/**
|
||||
* 机器人指令帮助注解
|
||||
*
|
||||
* @Annotation
|
||||
* @NamedArgumentConstructor()
|
||||
* @Target("METHOD")
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class CommandHelp extends AnnotationBase
|
||||
{
|
||||
public function __construct(
|
||||
public string $description,
|
||||
public string $usage,
|
||||
public string $example,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function make(
|
||||
string $description,
|
||||
string $usage,
|
||||
string $example,
|
||||
): CommandHelp {
|
||||
return new static(...func_get_args());
|
||||
}
|
||||
}
|
||||
@@ -50,13 +50,13 @@ class InitCommand extends Command
|
||||
$section->write('<fg=gray>更新 composer.json ... </>');
|
||||
|
||||
if (!file_exists($this->base_path . '/composer.json')) {
|
||||
throw new InitException('未找到 composer.json 文件', '请检查当前目录是否为项目根目录', 41);
|
||||
throw new InitException('未找到 composer.json 文件', 41);
|
||||
}
|
||||
|
||||
try {
|
||||
$composer = json_decode(file_get_contents($this->base_path . '/composer.json'), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
throw new InitException('解析 composer.json 文件失败', '请检查 composer.json 文件是否存在语法错误', 42, $e);
|
||||
throw new InitException('解析 composer.json 文件失败', 42, $e);
|
||||
}
|
||||
|
||||
if (!isset($composer['autoload'])) {
|
||||
@@ -68,7 +68,7 @@ class InitCommand extends Command
|
||||
try {
|
||||
file_put_contents($this->base_path . '/composer.json', json_encode($composer, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
} catch (\JsonException $e) {
|
||||
throw new InitException('写入 composer.json 文件失败', '', 0, $e);
|
||||
throw new InitException('写入 composer.json 文件失败', 0, $e);
|
||||
}
|
||||
|
||||
$section->writeln('<info>完成</info>');
|
||||
@@ -146,7 +146,7 @@ class InitCommand extends Command
|
||||
if (file_exists($phar_link . '/vendor/autoload.php')) {
|
||||
$this->base_path = $current_dir;
|
||||
} else {
|
||||
throw new InitException('框架启动模式不是 Composer 模式,无法进行初始化', '如果您是从 Github 下载的框架,请参阅文档进行源码模式启动', 42);
|
||||
throw new InitException('框架启动模式不是 Composer 模式,无法进行初始化', 42);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,11 +166,11 @@ class InitCommand extends Command
|
||||
&& !mkdir($concurrent_dir = $this->base_path . $info['dirname'], 0777, true)
|
||||
&& !is_dir($concurrent_dir)
|
||||
) {
|
||||
throw new InitException("无法创建目录 {$concurrent_dir}", '请检查目录权限');
|
||||
throw new InitException("无法创建目录 {$concurrent_dir}");
|
||||
}
|
||||
|
||||
if (copy($this->getVendorPath($file), $this->base_path . $file) === false) {
|
||||
throw new InitException("无法复制文件 {$file}", '请检查目录权限');
|
||||
throw new InitException("无法复制文件 {$file}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,22 +7,26 @@ namespace ZM\Container;
|
||||
use DI;
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use OneBot\Driver\Coroutine\Adaptive;
|
||||
|
||||
class ContainerHolder
|
||||
{
|
||||
private static ?Container $container = null;
|
||||
/** @var Container[] */
|
||||
private static array $container = [];
|
||||
|
||||
public static function getEventContainer(): Container
|
||||
{
|
||||
if (self::$container === null) {
|
||||
self::$container = self::buildContainer();
|
||||
$cid = Adaptive::getCoroutine()?->getCid() ?? -1;
|
||||
if (!isset(self::$container[$cid])) {
|
||||
self::$container[$cid] = self::buildContainer();
|
||||
}
|
||||
return self::$container;
|
||||
return self::$container[$cid];
|
||||
}
|
||||
|
||||
public static function clearEventContainer(): void
|
||||
{
|
||||
self::$container = null;
|
||||
$cid = Adaptive::getCoroutine()?->getCid() ?? -1;
|
||||
unset(self::$container[$cid]);
|
||||
}
|
||||
|
||||
private static function buildContainer(): Container
|
||||
|
||||
@@ -26,8 +26,16 @@ class ContainerRegistrant
|
||||
self::addServices([
|
||||
OneBotEvent::class => $event,
|
||||
'bot.event' => DI\get(OneBotEvent::class),
|
||||
BotContext::class => fn () => bot(),
|
||||
]);
|
||||
|
||||
if (isset($event->self['platform'])) {
|
||||
self::addServices([
|
||||
BotContext::class => DI\autowire(BotContext::class)->constructor(
|
||||
$event->self['user_id'] ?? '',
|
||||
$event->self['platform'],
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,22 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Context;
|
||||
|
||||
use Choir\Http\HttpFactory;
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||||
use OneBot\Util\Utils;
|
||||
use OneBot\V12\Object\Action;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use OneBot\V12\Object\OneBotEvent;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\OneBot\BotAction;
|
||||
use ZM\Context\Trait\BotActionTrait;
|
||||
use ZM\Exception\OneBot12Exception;
|
||||
use ZM\Utils\MessageUtil;
|
||||
|
||||
class BotContext implements ContextInterface
|
||||
{
|
||||
use BotActionTrait;
|
||||
|
||||
private static array $echo_id_list = [];
|
||||
|
||||
private array $self;
|
||||
@@ -28,9 +27,10 @@ class BotContext implements ContextInterface
|
||||
|
||||
private bool $replied = false;
|
||||
|
||||
public function __construct(string $bot_id, string $platform)
|
||||
public function __construct(string $bot_id, string $platform, null|WebSocketMessageEvent|HttpRequestEvent $event = null)
|
||||
{
|
||||
$this->self = ['user_id' => $bot_id, 'platform' => $platform];
|
||||
$this->base_event = $event;
|
||||
}
|
||||
|
||||
public function getEvent(): OneBotEvent
|
||||
@@ -82,21 +82,9 @@ class BotContext implements ContextInterface
|
||||
*/
|
||||
public function getBot(string $bot_id, string $platform = ''): BotContext
|
||||
{
|
||||
// TODO: 完善多机器人支持
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function sendMessage(\Stringable|array|MessageSegment|string $message, string $detail_type, array $params = [])
|
||||
{
|
||||
$message = MessageUtil::convertToArr($message);
|
||||
$params['message'] = $message;
|
||||
$params['detail_type'] = $detail_type;
|
||||
return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->self);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置该消息下解析出来的参数列表
|
||||
*
|
||||
@@ -134,39 +122,4 @@ class BotContext implements ContextInterface
|
||||
{
|
||||
return self::$echo_id_list[$echo] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function sendAction(string $action, array $params = [], ?array $self = null)
|
||||
{
|
||||
// 声明 Action 对象
|
||||
$a = new Action($action, $params, ob_uuidgen(), $self);
|
||||
self::$echo_id_list[$a->echo] = $a;
|
||||
// 调用事件在回复之前的回调
|
||||
$handler = new AnnotationHandler(BotAction::class);
|
||||
container()->set(Action::class, $a);
|
||||
$handler->setRuleCallback(fn (BotAction $act) => $act->action === '' || $act->action === $action && !$act->need_response);
|
||||
$handler->handleAll($a);
|
||||
// 被阻断时候,就不发送了
|
||||
if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用机器人连接发送 Action
|
||||
if (container()->has('ws.message.event')) {
|
||||
/** @var WebSocketMessageEvent $ws */
|
||||
$ws = container()->get('ws.message.event');
|
||||
return $ws->send(json_encode($a->jsonSerialize()));
|
||||
}
|
||||
// 如果是 HTTP WebHook 的形式,那么直接调用 Response
|
||||
if (container()->has('http.request.event')) {
|
||||
/** @var HttpRequestEvent $event */
|
||||
$event = container()->get('http.request.event');
|
||||
$response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()]));
|
||||
$event->withResponse($response);
|
||||
return true;
|
||||
}
|
||||
throw new OneBot12Exception('No bot connection found.');
|
||||
}
|
||||
}
|
||||
|
||||
81
src/ZM/Context/Trait/BotActionTrait.php
Normal file
81
src/ZM/Context/Trait/BotActionTrait.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Context\Trait;
|
||||
|
||||
use Choir\Http\HttpFactory;
|
||||
use OneBot\Driver\Event\Http\HttpRequestEvent;
|
||||
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
|
||||
use OneBot\Util\Utils;
|
||||
use OneBot\V12\Object\Action;
|
||||
use OneBot\V12\Object\ActionResponse;
|
||||
use OneBot\V12\Object\MessageSegment;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
use ZM\Annotation\OneBot\BotAction;
|
||||
use ZM\Exception\OneBot12Exception;
|
||||
use ZM\Utils\MessageUtil;
|
||||
|
||||
trait BotActionTrait
|
||||
{
|
||||
private null|WebSocketMessageEvent|HttpRequestEvent $base_event;
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function sendMessage(\Stringable|array|MessageSegment|string $message, string $detail_type, array $params = []): ActionResponse|bool
|
||||
{
|
||||
$message = MessageUtil::convertToArr($message);
|
||||
$params['message'] = $message;
|
||||
$params['detail_type'] = $detail_type;
|
||||
return $this->sendAction(Utils::camelToSeparator(__FUNCTION__), $params, $this->self);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送机器人动作
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function sendAction(string $action, array $params = [], ?array $self = null): bool|ActionResponse
|
||||
{
|
||||
// 声明 Action 对象
|
||||
$a = new Action($action, $params, ob_uuidgen(), $self);
|
||||
self::$echo_id_list[$a->echo] = $a;
|
||||
// 调用事件在回复之前的回调
|
||||
$handler = new AnnotationHandler(BotAction::class);
|
||||
container()->set(Action::class, $a);
|
||||
$handler->setRuleCallback(fn (BotAction $act) => $act->action === '' || $act->action === $action && !$act->need_response);
|
||||
$handler->handleAll($a);
|
||||
// 被阻断时候,就不发送了
|
||||
if ($handler->getStatus() === AnnotationHandler::STATUS_INTERRUPTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用机器人连接发送 Action
|
||||
if ($this->base_event instanceof WebSocketMessageEvent) {
|
||||
$result = $this->base_event->send(json_encode($a->jsonSerialize()));
|
||||
}
|
||||
if (!isset($result) && container()->has('ws.message.event')) {
|
||||
$result = container()->get('ws.message.event')->send(json_encode($a->jsonSerialize()));
|
||||
}
|
||||
// 如果是 HTTP WebHook 的形式,那么直接调用 Response
|
||||
if (!isset($result) && $this->base_event instanceof HttpRequestEvent) {
|
||||
$response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()]));
|
||||
$this->base_event->withResponse($response);
|
||||
$result = true;
|
||||
}
|
||||
if (!isset($result) && container()->has('http.request.event')) {
|
||||
$response = HttpFactory::createResponse(headers: ['Content-Type' => 'application/json'], body: json_encode([$a->jsonSerialize()]));
|
||||
container()->get('http.request.event')->withResponse($response);
|
||||
$result = true;
|
||||
}
|
||||
if (isset($result)) {
|
||||
return $result;
|
||||
}
|
||||
/* TODO: 协程支持
|
||||
if (($result ?? false) === true && ($co = Adaptive::getCoroutine()) !== null) {
|
||||
return $result ?? false;
|
||||
}*/
|
||||
throw new OneBot12Exception('No bot connection found.');
|
||||
}
|
||||
}
|
||||
@@ -119,10 +119,10 @@ class SignalListener
|
||||
}
|
||||
echo "\r";
|
||||
logger()->notice('请再按 {count} 次 Ctrl+C 以强制杀死进程', ['count' => 5 - self::$manager_kill_time]);
|
||||
return;
|
||||
}
|
||||
++self::$manager_kill_time;
|
||||
if (self::$manager_kill_time === 1) {
|
||||
logger()->notice('Keyboard interrupt, shutting down server...');
|
||||
Framework::getInstance()->stop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ZM\Event\Listener;
|
||||
|
||||
use OneBot\Driver\Coroutine\Adaptive;
|
||||
use OneBot\Driver\Process\ProcessManager;
|
||||
use OneBot\Util\Singleton;
|
||||
use ZM\Annotation\AnnotationHandler;
|
||||
@@ -12,12 +13,14 @@ use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\Init;
|
||||
use ZM\Exception\ZMKnownException;
|
||||
use ZM\Framework;
|
||||
use ZM\Plugin\CommandManualPlugin;
|
||||
use ZM\Plugin\OneBot12Adapter;
|
||||
use ZM\Plugin\PluginManager;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Store\Database\DBException;
|
||||
use ZM\Store\Database\DBPool;
|
||||
use ZM\Store\FileSystem;
|
||||
use ZM\Store\KV\LightCache;
|
||||
use ZM\Utils\ZMUtil;
|
||||
|
||||
class WorkerEventListener
|
||||
@@ -34,6 +37,8 @@ class WorkerEventListener
|
||||
// 自注册一下,刷新当前进程的logger进程banner
|
||||
ob_logger_register(ob_logger());
|
||||
|
||||
Adaptive::initWithDriver(Framework::getInstance()->getDriver());
|
||||
|
||||
// 如果没有引入参数disable-safe-exit,则监听 Ctrl+C
|
||||
if (!Framework::getInstance()->getArgv()['disable-safe-exit'] && PHP_OS_FAMILY !== 'Windows') {
|
||||
SignalListener::getInstance()->signalWorker();
|
||||
@@ -41,12 +46,10 @@ class WorkerEventListener
|
||||
|
||||
// Windows 环境下,为了监听 Ctrl+C,只能开启终端输入
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
logger()->debug('监听Windows的键盘输入');
|
||||
sapi_windows_set_ctrl_handler([SignalListener::getInstance(), 'signalWindowsCtrlC']);
|
||||
Framework::getInstance()->getDriver()->getEventLoop()->addReadEvent(STDIN, function ($x) {});
|
||||
}
|
||||
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' started');
|
||||
|
||||
// 设置 Worker 进程的状态和 ID 等信息
|
||||
if (($name = Framework::getInstance()->getDriver()->getName()) === 'swoole') {
|
||||
/* @phpstan-ignore-next-line */
|
||||
@@ -68,6 +71,11 @@ class WorkerEventListener
|
||||
logger()->info('WORKER#' . $i . ":\t" . ProcessStateManager::getProcessState(ZM_PROCESS_WORKER, $i));
|
||||
}
|
||||
|
||||
// 如果使用的是 LightCache,注册下自动保存的监听器
|
||||
if (is_a(config('global.kv.use', \LightCache::class), LightCache::class, true)) {
|
||||
Framework::getInstance()->getDriver()->getEventLoop()->addTimer(config('global.kv.light_cache_autosave_time', 600) * 1000, [LightCache::class, 'saveAll'], 0);
|
||||
}
|
||||
|
||||
// 注册 Worker 进程遇到退出时的回调,安全退出
|
||||
register_shutdown_function(function () {
|
||||
$error = error_get_last();
|
||||
@@ -87,18 +95,28 @@ class WorkerEventListener
|
||||
$this->initUserPlugins();
|
||||
|
||||
// handle @Init annotation
|
||||
$this->dispatchInit();
|
||||
|
||||
Adaptive::getCoroutine()->create(function () {
|
||||
$this->dispatchInit();
|
||||
});
|
||||
// 回显 debug 日志:进程占用的内存
|
||||
$memory_total = memory_get_usage() / 1024 / 1024;
|
||||
logger()->debug('Worker process used ' . round($memory_total, 3) . ' MB');
|
||||
}
|
||||
|
||||
public function onWorkerStart1(): void
|
||||
{
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' started');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ZMKnownException
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function onWorkerStop999()
|
||||
public function onWorkerStop999(): void
|
||||
{
|
||||
if (is_a(config('global.kv.use', \LightCache::class), LightCache::class, true)) {
|
||||
LightCache::saveAll();
|
||||
}
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' stopping');
|
||||
if (DIRECTORY_SEPARATOR !== '\\') {
|
||||
ProcessStateManager::removeProcessState(ZM_PROCESS_WORKER, ProcessManager::getProcessId());
|
||||
@@ -109,11 +127,16 @@ class WorkerEventListener
|
||||
}
|
||||
}
|
||||
|
||||
public function onWorkerStop1(): void
|
||||
{
|
||||
logger()->debug('Worker #' . ProcessManager::getProcessId() . ' stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户代码资源,包括普通插件、单文件插件、Composer 插件等
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function initUserPlugins()
|
||||
private function initUserPlugins(): void
|
||||
{
|
||||
logger()->debug('Loading user sources');
|
||||
|
||||
@@ -141,6 +164,7 @@ class WorkerEventListener
|
||||
match ($name) {
|
||||
'onebot12' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(parser: $parser)]),
|
||||
'onebot12-ban-other-ws' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new OneBot12Adapter(submodule: $name)]),
|
||||
'command-manual' => PluginManager::addPlugin(['name' => $name, 'internal' => true, 'object' => new CommandManualPlugin($parser)]),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ class ConfigException extends ZMException
|
||||
|
||||
public static function unsupportedFileType(string $file_path): ConfigException
|
||||
{
|
||||
return new self("不支持的配置文件类型:{$file_path}", '请检查配置文件的后缀名是否正确', self::UNSUPPORTED_FILE_TYPE);
|
||||
return new self("不支持的配置文件类型:{$file_path}", self::UNSUPPORTED_FILE_TYPE);
|
||||
}
|
||||
|
||||
public static function loadConfigFailed(string $file_path, string $message): ConfigException
|
||||
{
|
||||
return new self("加载配置文件失败:{$file_path},{$message}", '请检查配置文件的格式是否正确,并尝试按照错误信息排查', self::LOAD_CONFIG_FAILED);
|
||||
return new self("加载配置文件失败:{$file_path},{$message}", self::LOAD_CONFIG_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,23 @@ declare(strict_types=1);
|
||||
namespace ZM\Exception;
|
||||
|
||||
use OneBot\Exception\ExceptionHandler;
|
||||
use OneBot\Exception\ExceptionHandlerInterface;
|
||||
use ZM\Exception\Solution\SolutionRepository;
|
||||
|
||||
class Handler extends ExceptionHandler implements ExceptionHandlerInterface
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
/** @noinspection ClassConstantCanBeUsedInspection */
|
||||
$ns = 'NunoMaduro\Collision\Handler';
|
||||
// TODO: 在 LibOB 发布新版时移除检查
|
||||
if (class_exists($ns) && method_exists($this, 'tryEnableCollision')) {
|
||||
$this->tryEnableCollision(new SolutionRepository());
|
||||
}
|
||||
}
|
||||
|
||||
public function handle(\Throwable $e): void
|
||||
{
|
||||
if ($e instanceof ZMKnownException) {
|
||||
// 如果是已知异常,则可以输出问题说明和解决方案
|
||||
// TODO
|
||||
}
|
||||
|
||||
$this->handle0($e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ class InterruptException extends ZMException
|
||||
{
|
||||
public function __construct(public $return_var = null, $message = '', $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, '', $code, $previous);
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,4 @@ namespace ZM\Exception;
|
||||
|
||||
class InvalidArgumentException extends ZMException
|
||||
{
|
||||
public function __construct($message = '', $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
// TODO: change this to a better error message
|
||||
parent::__construct($message, '', $code ?: 74, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ class SingletonViolationException extends ZMException
|
||||
{
|
||||
parent::__construct(
|
||||
"类 {$singleton_class_name} 是单例模式,不允许初始化多个实例。",
|
||||
"请检查代码,确保只初始化了一个 {$singleton_class_name} 实例。",
|
||||
69
|
||||
);
|
||||
}
|
||||
|
||||
30
src/ZM/Exception/Solution/Solution.php
Normal file
30
src/ZM/Exception/Solution/Solution.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Exception\Solution;
|
||||
|
||||
class Solution
|
||||
{
|
||||
public function __construct(
|
||||
private string $title,
|
||||
private string $description,
|
||||
private array $links,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getSolutionTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getSolutionDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function getDocumentationLinks(): array
|
||||
{
|
||||
return $this->links;
|
||||
}
|
||||
}
|
||||
22
src/ZM/Exception/Solution/SolutionRepository.php
Normal file
22
src/ZM/Exception/Solution/SolutionRepository.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Exception\Solution;
|
||||
|
||||
use NunoMaduro\Collision\Contracts\SolutionsRepository;
|
||||
|
||||
class SolutionRepository implements SolutionsRepository
|
||||
{
|
||||
/**
|
||||
* @return Solution[]
|
||||
*/
|
||||
public function getFromThrowable(\Throwable $throwable): array
|
||||
{
|
||||
return match ($throwable::class) {
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class WaitTimeoutException extends ZMException
|
||||
|
||||
public function __construct($module, $message = '', $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, '', $code, $previous);
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->module = $module;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,4 @@ namespace ZM\Exception;
|
||||
|
||||
abstract class ZMException extends \Exception
|
||||
{
|
||||
public function __construct(string $description, string $solution = '', int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($description . PHP_EOL . $solution, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class ZMKnownException extends ZMException
|
||||
{
|
||||
public function __construct($err_code, $message = '', $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(zm_internal_errcode($err_code) . $message, '', $code, $previous);
|
||||
parent::__construct(zm_internal_errcode($err_code) . $message, $code, $previous);
|
||||
if ($err_code === 'E99999') {
|
||||
$code = 0;
|
||||
// 这也太懒了吧
|
||||
@@ -19,6 +19,6 @@ class ZMKnownException extends ZMException
|
||||
// 取最后两数
|
||||
$code = (int) substr($err_code, -2);
|
||||
}
|
||||
parent::__construct($message, '', $code, $previous);
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,16 +43,16 @@ class Framework
|
||||
use Singleton;
|
||||
|
||||
/** @var int 版本ID */
|
||||
public const VERSION_ID = 646;
|
||||
public const VERSION_ID = 654;
|
||||
|
||||
/** @var string 版本名称 */
|
||||
public const VERSION = '3.0.0-beta2';
|
||||
public const VERSION = '3.0.0-beta3';
|
||||
|
||||
/** @var array 传入的参数 */
|
||||
protected array $argv;
|
||||
|
||||
/** @var Driver|SwooleDriver|WorkermanDriver OneBot驱动 */
|
||||
protected SwooleDriver|Driver|WorkermanDriver $driver;
|
||||
/** @var null|Driver|SwooleDriver|WorkermanDriver OneBot驱动 */
|
||||
protected SwooleDriver|Driver|WorkermanDriver|null $driver = null;
|
||||
|
||||
/** @var array<array<string, string>> 启动注解列表 */
|
||||
protected array $setup_annotations = [];
|
||||
@@ -178,6 +178,9 @@ class Framework
|
||||
*/
|
||||
public function getDriver(): Driver
|
||||
{
|
||||
if ($this->driver === null) {
|
||||
$this->driver = new WorkermanDriver();
|
||||
}
|
||||
return $this->driver;
|
||||
}
|
||||
|
||||
@@ -228,7 +231,9 @@ class Framework
|
||||
// 添加框架需要监听的顶层事件监听器
|
||||
// worker 事件
|
||||
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart999'], 999);
|
||||
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStart1'], 1);
|
||||
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop999'], 999);
|
||||
ob_event_provider()->addEventListener(WorkerStopEvent::getName(), [WorkerEventListener::getInstance(), 'onWorkerStop1'], 1);
|
||||
// Http 事件
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest999'], 999);
|
||||
ob_event_provider()->addEventListener(HttpRequestEvent::getName(), [HttpEventListener::getInstance(), 'onRequest1'], 1);
|
||||
|
||||
127
src/ZM/Plugin/CommandManualPlugin.php
Normal file
127
src/ZM/Plugin/CommandManualPlugin.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Plugin;
|
||||
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\OneBot\BotCommand;
|
||||
use ZM\Annotation\OneBot\CommandArgument;
|
||||
use ZM\Annotation\OneBot\CommandHelp;
|
||||
use ZM\Context\BotContext;
|
||||
|
||||
/**
|
||||
* CommandManual 插件
|
||||
*
|
||||
* 用以生成、处理指令帮助
|
||||
*/
|
||||
class CommandManualPlugin extends ZMPlugin
|
||||
{
|
||||
private array $template = [
|
||||
['type' => 'command', 'header' => false, 'indent' => false],
|
||||
['type' => 'description', 'header' => false, 'indent' => false],
|
||||
['type' => 'usage', 'header' => false, 'indent' => false],
|
||||
['type' => 'arguments', 'header' => '可用参数:', 'indent' => true],
|
||||
['type' => 'examples', 'header' => '使用示例:', 'indent' => true],
|
||||
];
|
||||
|
||||
/**
|
||||
* 命令(帮助)列表,键为命令名,值为命令帮助
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $command_list = [];
|
||||
|
||||
public function __construct(AnnotationParser $parser)
|
||||
{
|
||||
parent::__construct(__DIR__);
|
||||
|
||||
if (config('command_manual.template') !== null) {
|
||||
$this->template = config('command_manual.template');
|
||||
}
|
||||
|
||||
$parser->addSpecialParser(BotCommand::class, [$this, 'parseBotCommand']);
|
||||
$parser->addSpecialParser(CommandHelp::class, fn () => false);
|
||||
|
||||
$this->addBotCommand(
|
||||
BotCommand::make('help', 'help')
|
||||
->withArgument('command', '要查询的指令名', required: true)
|
||||
->on([$this, 'onHelp'])
|
||||
);
|
||||
logger()->info('CommandManualPlugin loaded.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 BotCommand 的参数和帮助
|
||||
*
|
||||
* @param BotCommand $command 命令对象
|
||||
* @param null|array $same_method_annotations 同一个方法的所有注解
|
||||
*/
|
||||
public function parseBotCommand(BotCommand $command, ?array $same_method_annotations = null): ?bool
|
||||
{
|
||||
if ($same_method_annotations) {
|
||||
foreach ($same_method_annotations as $v) {
|
||||
if ($v instanceof CommandHelp) {
|
||||
$help = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$help = $help ?? new CommandHelp('', '', '');
|
||||
$section = '';
|
||||
foreach ($this->template as $v) {
|
||||
$content = $this->getSectionContent($command, $v['type'], $help);
|
||||
$this->addSection($section, $content, $v);
|
||||
}
|
||||
$this->command_list[$command->name] = $section;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onHelp(BotContext $context): void
|
||||
{
|
||||
$command = $context->getParam('command');
|
||||
if (isset($this->command_list[$command])) {
|
||||
$context->reply($this->command_list[$command]);
|
||||
} else {
|
||||
$context->reply('未找到指令 ' . $command);
|
||||
}
|
||||
}
|
||||
|
||||
private function addSection(string &$section, string $content, array $options): void
|
||||
{
|
||||
if (!$content) {
|
||||
return;
|
||||
}
|
||||
if ($options['header']) {
|
||||
$section .= $options['header'] . PHP_EOL;
|
||||
}
|
||||
if ($options['indent']) {
|
||||
$content = ' ' . str_replace(PHP_EOL, PHP_EOL . ' ', $content);
|
||||
$content = rtrim($content);
|
||||
}
|
||||
$section .= $content . PHP_EOL;
|
||||
}
|
||||
|
||||
private function getSectionContent(BotCommand $command, string $type, CommandHelp $help): string
|
||||
{
|
||||
switch ($type) {
|
||||
case 'command':
|
||||
return $command->name;
|
||||
case 'description':
|
||||
return $help->description;
|
||||
case 'usage':
|
||||
return $help->usage;
|
||||
case 'arguments':
|
||||
$ret = '';
|
||||
foreach ($command->getArguments() as $argument) {
|
||||
/* @var CommandArgument $argument */
|
||||
$ret .= $argument->name . ' - ' . $argument->description . PHP_EOL;
|
||||
}
|
||||
return $ret;
|
||||
case 'examples':
|
||||
return $help->example;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace ZM\Plugin;
|
||||
use ZM\Annotation\AnnotationMap;
|
||||
use ZM\Annotation\AnnotationParser;
|
||||
use ZM\Annotation\Framework\BindEvent;
|
||||
use ZM\Annotation\OneBot\BotCommand;
|
||||
use ZM\Annotation\OneBot\BotEvent;
|
||||
use ZM\Exception\PluginException;
|
||||
use ZM\Store\FileSystem;
|
||||
@@ -191,6 +192,7 @@ class PluginManager
|
||||
}
|
||||
// 将 BotCommand 加入事件监听
|
||||
foreach ($obj->getBotCommands() as $cmd) {
|
||||
AnnotationMap::$_list[BotCommand::class][] = $cmd;
|
||||
$parser->parseSpecial($cmd);
|
||||
}
|
||||
} elseif (isset($plugin['autoload'], $plugin['dir'])) {
|
||||
|
||||
@@ -10,6 +10,6 @@ class DBException extends ZMException
|
||||
{
|
||||
public function __construct(string $description, int $code = 0, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($description, '', $code, $previous);
|
||||
parent::__construct($description, $code, $previous);
|
||||
}
|
||||
}
|
||||
|
||||
47
src/ZM/Store/KV/KVInterface.php
Normal file
47
src/ZM/Store/KV/KVInterface.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
interface KVInterface
|
||||
{
|
||||
/**
|
||||
* 打开一个 KV 库
|
||||
*
|
||||
* @param string $name KV 的库名称
|
||||
*/
|
||||
public static function open(string $name = ''): KVInterface;
|
||||
|
||||
/**
|
||||
* 返回一个 KV 键值对的数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param null|mixed $default 如果不存在时返回的默认值
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed;
|
||||
|
||||
/**
|
||||
* 设置一个 KV 键值对的数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @param mixed $value 键值
|
||||
* @param int $ttl 超时秒数(如果等于 0 代表永不超时)
|
||||
*/
|
||||
public function set(string $key, mixed $value, int $ttl = 0): bool;
|
||||
|
||||
/**
|
||||
* 强制删除一个 KV 键值对数据
|
||||
*
|
||||
* @param string $key 键名
|
||||
* @return bool 当键存在并被删除时返回 true
|
||||
*/
|
||||
public function unset(string $key): bool;
|
||||
|
||||
/**
|
||||
* 键值对数据是否存在
|
||||
*
|
||||
* @param string $key 键名
|
||||
*/
|
||||
public function isset(string $key): bool;
|
||||
}
|
||||
164
src/ZM/Store/KV/LightCache.php
Normal file
164
src/ZM/Store/KV/LightCache.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
/**
|
||||
* 轻量、基于本地 JSON 文件的 KV 键值对缓存
|
||||
*/
|
||||
class LightCache implements KVInterface
|
||||
{
|
||||
/** @var array 存放库对象的列表 */
|
||||
private static array $objs = [];
|
||||
|
||||
/** @var array 存放缓存数据的列表 */
|
||||
private static array $caches = [];
|
||||
|
||||
/** @var array 存放超时数据的列表 */
|
||||
private static array $ttys = [];
|
||||
|
||||
/** @var string 查找库的目录地址 */
|
||||
private string $find_dir;
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(private string $name = '', string $find_str = '')
|
||||
{
|
||||
if ((ProcessStateManager::$process_mode['worker'] ?? 0) > 1) {
|
||||
logger()->error('LightCache 不支持多进程模式,如需在多进程下使用,请使用 ZMRedis 作为 KV 引擎!');
|
||||
return;
|
||||
}
|
||||
$this->find_dir = empty($find_str) ? config('global.kv.light_cache_dir', '/tmp/zm_light_cache') : $find_str;
|
||||
FileSystem::createDir($this->find_dir);
|
||||
$this->validateKey($name);
|
||||
if (file_exists($this->find_dir . '/' . $name . '.json')) {
|
||||
$data = json_decode(file_get_contents($this->find_dir . '/' . $name . '.json'), true);
|
||||
if (is_array($data)) {
|
||||
self::$caches[$name] = $data['data'];
|
||||
self::$ttys[$name] = $data['expire'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function open(string $name = ''): KVInterface
|
||||
{
|
||||
if (!isset(self::$objs[$name])) {
|
||||
self::$objs[$name] = new LightCache($name);
|
||||
}
|
||||
return self::$objs[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public static function saveAll()
|
||||
{
|
||||
/** @var LightCache $obj */
|
||||
foreach (self::$objs as $obj) {
|
||||
$obj->save();
|
||||
}
|
||||
logger()->debug('Saved all light caches');
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 KV 库的数据到文件
|
||||
*
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
file_put_contents(zm_dir($this->find_dir . '/' . $this->name . '.json'), json_encode([
|
||||
'data' => self::$caches[$this->name] ?? [],
|
||||
'expire' => self::$ttys[$this->name] ?? [],
|
||||
], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除该 KV 库的所有数据,并且永远无法恢复
|
||||
*/
|
||||
public function removeSelf(): bool
|
||||
{
|
||||
if (file_exists($this->find_dir . '/' . $this->name . '.json')) {
|
||||
unlink($this->find_dir . '/' . $this->name . '.json');
|
||||
}
|
||||
unset(self::$caches[$this->name], self::$ttys[$this->name], self::$objs[$this->name]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
// 首先判断在不在缓存变量里
|
||||
if (!isset(self::$caches[$this->name][$key])) {
|
||||
return $default;
|
||||
}
|
||||
// 然后判断是否有延迟
|
||||
if (isset(self::$ttys[$this->name][$key])) {
|
||||
if (self::$ttys[$this->name][$key] > time()) {
|
||||
return self::$caches[$this->name][$key];
|
||||
}
|
||||
unset(self::$ttys[$this->name][$key], self::$caches[$this->name][$key]);
|
||||
|
||||
return $default;
|
||||
}
|
||||
return self::$caches[$this->name][$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function set(string $key, mixed $value, int $ttl = 0): bool
|
||||
{
|
||||
$this->validateKey($key);
|
||||
self::$caches[$this->name][$key] = $value;
|
||||
if ($ttl > 0) {
|
||||
self::$ttys[$this->name][$key] = time() + $ttl;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function unset(string $key): bool
|
||||
{
|
||||
unset(self::$caches[$this->name][$key], self::$ttys[$this->name][$key]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isset(string $key): bool
|
||||
{
|
||||
if (!isset(self::$caches[$this->name][$key])) {
|
||||
return false;
|
||||
}
|
||||
if (isset(self::$ttys[$this->name][$key])) {
|
||||
if (self::$ttys[$this->name][$key] > time()) {
|
||||
return true;
|
||||
}
|
||||
unset(self::$ttys[$this->name][$key], self::$caches[$this->name][$key]);
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateKey(string $key): void
|
||||
{
|
||||
if ($key === '') {
|
||||
return;
|
||||
}
|
||||
if (strlen($key) >= 128) {
|
||||
throw new InvalidArgumentException('LightCache 键名长度不能超过 128 字节!');
|
||||
}
|
||||
// 只能包含数字、大小写字母、下划线、短横线、点、中文
|
||||
if (!preg_match('/^[\w\-.\x{4e00}-\x{9fa5}]+$/u', $key)) {
|
||||
throw new InvalidArgumentException('LightCache 键名只能包含数字、大小写字母、下划线、短横线、点、中文!');
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/ZM/Utils/ZMRequest.php
Normal file
72
src/ZM/Utils/ZMRequest.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Utils;
|
||||
|
||||
use OneBot\Util\Singleton;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use ZM\Framework;
|
||||
|
||||
class ZMRequest
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* 快速发起一个 GET 请求
|
||||
*
|
||||
* @param string|\Stringable|UriInterface $url 请求地址
|
||||
* @param array $headers 请求头
|
||||
* @param array $config 传入参数
|
||||
* @param bool $only_body 是否只返回 Response 的 body 部分,默认为 True
|
||||
* @return bool|ResponseInterface|string 返回 False 代表请求失败,返回 string 为仅 Body 的内容,返回 Response 接口对象表明是回包
|
||||
*/
|
||||
public static function get(string|UriInterface|\Stringable $url, array $headers = [], array $config = [], bool $only_body = true): bool|ResponseInterface|string
|
||||
{
|
||||
$socket = Framework::getInstance()->getDriver()->createHttpClientSocket(array_merge_recursive([
|
||||
'url' => ($url instanceof UriInterface ? $url->__toString() : $url),
|
||||
], $config));
|
||||
$socket->withoutAsync();
|
||||
$obj = $socket->get($headers, function (ResponseInterface $response) { return $response; }, function () { return false; });
|
||||
if ($obj instanceof ResponseInterface) {
|
||||
if ($obj->getStatusCode() !== 200 && $only_body) {
|
||||
return false;
|
||||
}
|
||||
if (!$only_body) {
|
||||
return $obj;
|
||||
}
|
||||
return $obj->getBody()->getContents();
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速发起一个 POST 请求
|
||||
*
|
||||
* @param string|\Stringable|UriInterface $url 请求地址
|
||||
* @param array $header 请求头
|
||||
* @param mixed $data 请求数据,当传入了一个可以 Json 化的对象时,自动 json_encode,其他情况须传入可字符串化的变量
|
||||
* @param array $config 传入参数
|
||||
* @param bool $only_body 是否只返回 Response 的 body 部分,默认为 True
|
||||
* @return bool|ResponseInterface|string 返回 False 代表请求失败,返回 string 为仅 Body 的内容,返回 Response 接口对象表明是回包
|
||||
*/
|
||||
public static function post(string|UriInterface|\Stringable $url, array $header, mixed $data, array $config = [], bool $only_body = true): bool|ResponseInterface|string
|
||||
{
|
||||
$socket = Framework::getInstance()->getDriver()->createHttpClientSocket(array_merge_recursive([
|
||||
'url' => ($url instanceof UriInterface ? $url->__toString() : $url),
|
||||
], $config));
|
||||
$socket->withoutAsync();
|
||||
$obj = $socket->post($data, $header, fn (ResponseInterface $response) => $response, fn () => false);
|
||||
if ($obj instanceof ResponseInterface) {
|
||||
if ($obj->getStatusCode() !== 200 && $only_body) {
|
||||
return false;
|
||||
}
|
||||
if (!$only_body) {
|
||||
return $obj;
|
||||
}
|
||||
return $obj->getBody()->getContents();
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ class ZMUtil
|
||||
{
|
||||
/**
|
||||
* 获取 composer.json 并转为数组进行读取使用
|
||||
* @param null|string $path 路径
|
||||
*
|
||||
* @param null|string $path 路径
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public static function getComposerMetadata(?string $path = null): ?array
|
||||
{
|
||||
|
||||
@@ -12,11 +12,14 @@ use ZM\Plugin\ZMPlugin;
|
||||
class ZMApplication extends ZMPlugin
|
||||
{
|
||||
/** @var null|ZMApplication 存储单例类的变量 */
|
||||
private static ?ZMApplication $obj;
|
||||
private static ?ZMApplication $obj = null;
|
||||
|
||||
/** @var array 存储要传入的args */
|
||||
private array $args = [];
|
||||
|
||||
/**
|
||||
* @throws SingletonViolationException
|
||||
*/
|
||||
public function __construct(mixed $dir = null)
|
||||
{
|
||||
if (self::$obj !== null) {
|
||||
|
||||
47
tests/ZM/Store/KV/LightCacheTest.php
Normal file
47
tests/ZM/Store/KV/LightCacheTest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\ZM\Store\KV;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ZM\Store\KV\LightCache;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LightCacheTest extends TestCase
|
||||
{
|
||||
public function testRemoveSelf()
|
||||
{
|
||||
$a = LightCache::open('asd');
|
||||
$this->assertInstanceOf(LightCache::class, $a);
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->assertTrue($a->removeSelf());
|
||||
}
|
||||
|
||||
public function testSet()
|
||||
{
|
||||
$this->assertTrue(LightCache::open()->set('test123', 'help'));
|
||||
}
|
||||
|
||||
public function testIsset()
|
||||
{
|
||||
$this->assertFalse(LightCache::open()->isset('test111'));
|
||||
}
|
||||
|
||||
public function testGet()
|
||||
{
|
||||
LightCache::open('ppp')->set('hello', 'world');
|
||||
$this->assertSame(LightCache::open('ppp')->get('hello', 'ffff'), 'world');
|
||||
}
|
||||
|
||||
public function testUnset()
|
||||
{
|
||||
$kv = LightCache::open('sss');
|
||||
$kv->set('test', 'test');
|
||||
$this->assertSame($kv->get('test'), 'test');
|
||||
$kv->unset('test');
|
||||
$this->assertNull($kv->get('test'));
|
||||
}
|
||||
}
|
||||
30
tests/ZM/Utils/ZMRequestTest.php
Normal file
30
tests/ZM/Utils/ZMRequestTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\ZM\Utils;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use ZM\Utils\ZMRequest;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ZMRequestTest extends TestCase
|
||||
{
|
||||
public function testPost()
|
||||
{
|
||||
$r = ZMRequest::post('http://httpbin.org/post', [], 'niubi=123');
|
||||
$this->assertStringContainsString('123', $r);
|
||||
$r2 = ZMRequest::post('http://httpbin.org/post', ['User-Agent' => 'test'], 'oijoij=ooo', [], false);
|
||||
$this->assertInstanceOf(ResponseInterface::class, $r2);
|
||||
$this->assertStringContainsString('ooo', $r2->getBody()->getContents());
|
||||
}
|
||||
|
||||
public function testGet()
|
||||
{
|
||||
$r = ZMRequest::get('http://ip.zhamao.xin');
|
||||
$this->assertStringContainsString('114', $r);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user