mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-05 07:45:37 +08:00
Compare commits
59 Commits
3.0.0-beta
...
3.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cd28964e6 | ||
|
|
d87b7dc0f7 | ||
|
|
1d27091558 | ||
|
|
3dafbf4fbd | ||
|
|
3a78f5e2b1 | ||
|
|
3f26648a3c | ||
|
|
a62e950870 | ||
|
|
aee7fa332a | ||
|
|
c1a0fae6e6 | ||
|
|
ed31edcc5c | ||
|
|
4aea90cb39 | ||
|
|
a58c3aadab | ||
|
|
f2fb40b67c | ||
|
|
18df76f650 | ||
|
|
dce126136b | ||
|
|
d32f7b0ff8 | ||
|
|
d55362e190 | ||
|
|
6bcedea720 | ||
|
|
ccc801e6cb | ||
|
|
a2404482a3 | ||
|
|
4c41dd09d2 | ||
|
|
ab83194bbe | ||
|
|
6013571267 | ||
|
|
2636bc2e35 | ||
|
|
a2b013402b | ||
|
|
206f041d29 | ||
|
|
ce885a7a61 | ||
|
|
b0d0d5eba9 | ||
|
|
d45c4e24fd | ||
|
|
49fffcc464 | ||
|
|
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 |
5
.github/workflows/increment-build-number.yml
vendored
5
.github/workflows/increment-build-number.yml
vendored
@@ -3,8 +3,7 @@ name: Increment Build Number
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- '*-develop'
|
||||
- main
|
||||
types:
|
||||
- closed
|
||||
paths:
|
||||
@@ -24,7 +23,7 @@ jobs:
|
||||
- name: Setup PHP
|
||||
uses: sunxyw/workflows/setup-environment@main
|
||||
with:
|
||||
php-version: 8.0
|
||||
php-version: 8.1
|
||||
php-extensions: swoole, posix, json
|
||||
operating-system: ubuntu-latest
|
||||
use-cache: true
|
||||
|
||||
12
.github/workflows/vuepress-deploy.yml
vendored
12
.github/workflows/vuepress-deploy.yml
vendored
@@ -2,7 +2,7 @@ name: Docs and Script Auto Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'ext/**'
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
SOURCE: "deploy/"
|
||||
REMOTE_HOST: ${{ secrets.ZHAMAO_XIN_HOST }}
|
||||
REMOTE_USER: ${{ secrets.ZHAMAO_XIN_USER }}
|
||||
TARGET: ${{ secrets.ZHAMAO_XIN_TARGET }}
|
||||
TARGET: ${{ secrets.FRAMEWORK_ZHAMAO_XIN_TARGET }}
|
||||
- name: deploy script file
|
||||
uses: wlixcc/SFTP-Deploy-Action@v1.2
|
||||
with:
|
||||
@@ -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 }}
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,7 +9,7 @@
|
||||
/site/
|
||||
/plugins/
|
||||
/doxy/
|
||||
|
||||
/walle/
|
||||
# 框架审计文件
|
||||
audit.log
|
||||
|
||||
@@ -82,3 +82,6 @@ package-lock.json
|
||||
/.tool-version
|
||||
|
||||
.DS_Store
|
||||
|
||||
### PHP CS Fixer ###
|
||||
.php-cs-fixer.cache
|
||||
|
||||
@@ -71,5 +71,6 @@ return (new PhpCsFixer\Config())
|
||||
PhpCsFixer\Finder::create()
|
||||
->in(__DIR__ . '/src')
|
||||
->in(__DIR__ . '/tests')
|
||||
)
|
||||
->setUsingCache(false);
|
||||
->in(__DIR__ . '/config')
|
||||
->in(__DIR__ . '/bin')
|
||||
);
|
||||
|
||||
22
README.md
22
README.md
@@ -57,8 +57,8 @@
|
||||
|
||||
```php
|
||||
#[\BotCommand('你好')]
|
||||
public function hello() {
|
||||
ctx()->reply("你好,我是炸毛!"); // 简单的命令式回复
|
||||
public function hello(\BotContext $ctx) {
|
||||
$ctx->reply("你好,我是炸毛!"); // 简单的命令式回复
|
||||
}
|
||||
#[\Route('/index')]
|
||||
public function index() {
|
||||
@@ -74,10 +74,10 @@ public function index() {
|
||||
|
||||
```bash
|
||||
# 检测PHP环境、安装框架
|
||||
bash <(curl -fsSL https://zhamao.xin/go.sh)
|
||||
bash <(curl -fsSL https://zhamao.xin/v3.sh)
|
||||
|
||||
# 启动框架
|
||||
cd zhamao-app
|
||||
cd zhamao-v3
|
||||
./zhamao server
|
||||
```
|
||||
|
||||
@@ -86,14 +86,14 @@ cd zhamao-app
|
||||
```bash
|
||||
# 脚本默认会检测系统的PHP,如果想直接跳过检测,安装独立的PHP版本,则添加此环境变量
|
||||
export ZM_NO_LOCAL_PHP="yes"
|
||||
# 脚本如果安装独立版本PHP,默认版本为8.0,如果想使用其他版本,则添加此环境变量指定版本
|
||||
export ZM_DOWN_PHP_VERSION="8.1"
|
||||
# 脚本如果安装独立版本PHP,默认版本为8.1,如果想使用其他版本,则添加此环境变量指定版本
|
||||
export ZM_DOWN_PHP_VERSION="8.2"
|
||||
# 脚本默认会将框架在当前目录下的 `zhamao-app` 目录进行安装,如果想使用其他目录,则添加此环境变量
|
||||
export ZM_CUSTOM_DIR="my-custom-app"
|
||||
# 脚本默认会对本项目使用阿里云国内加速镜像,如果想使用packagist源,则添加此环境变量
|
||||
export ZM_COMPOSER_PACKAGIST="yes"
|
||||
# 执行完前面的环境变量再执行一键安装脚本,就可以实现自定义参数!
|
||||
bash <(curl -fsSL https://zhamao.xin/go.sh)
|
||||
bash <(curl -fsSL https://zhamao.xin/v3.sh)
|
||||
```
|
||||
|
||||
关于其他安装方式,请参阅[文档](https://framework.zhamao.xin/guide/installation.html) 。
|
||||
@@ -116,12 +116,6 @@ bash <(curl -fsSL https://zhamao.xin/go.sh)
|
||||
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
|
||||
- 自带 PHP 环境,无需手动编译安装,by [crazywhalecc/static-php-cli](https://github.com/crazywhalecc/static-php-cli)
|
||||
|
||||
## 下载源码
|
||||
|
||||
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和克隆仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
|
||||
|
||||
例如:`git clone https://hub.fastgit.xyz/zhamao-robot/zhamao-framework.git --depth 1`。
|
||||
|
||||
## 贡献和捐赠
|
||||
|
||||
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
|
||||
@@ -142,7 +136,7 @@ bash <(curl -fsSL https://zhamao.xin/go.sh)
|
||||
|
||||
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
|
||||
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**四年半**,并且持续迭代。
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**五年**,并且持续迭代。
|
||||
|
||||
可以加作者 QQ([627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))
|
||||
或提交 [Issue](https://github.com/zhamao-robot/zhamao-framework/issues/new/choose) 进行疑难解答。
|
||||
|
||||
@@ -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
|
||||
)
|
||||
47
captainhook.json
Normal file
47
captainhook.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"pre-push": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer analyse"
|
||||
},
|
||||
{
|
||||
"action": "composer test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}",
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
|
||||
"args": ["php"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"post-change": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer install",
|
||||
"options": [],
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
|
||||
"args": [
|
||||
[
|
||||
"composer.json",
|
||||
"composer.lock"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,11 @@
|
||||
"dragonmantank/cron-expression": "^3.3",
|
||||
"jelix/version": "^2.0",
|
||||
"koriym/attributes": "^1.0",
|
||||
"nunomaduro/collision": "^6.3",
|
||||
"onebot/libonebot": "^0.5",
|
||||
"php-di/php-di": "^7",
|
||||
"psr/container": "^2.0",
|
||||
"psr/simple-cache": "^3.0",
|
||||
"psy/psysh": "^0.11.8",
|
||||
"symfony/console": "^6.0",
|
||||
"symfony/polyfill-ctype": "^1.19",
|
||||
@@ -30,7 +32,7 @@
|
||||
"symfony/routing": "~6.0 || ~5.0 || ~4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"brainmaestro/composer-git-hooks": "^3.0",
|
||||
"captainhook/captainhook": "^5.12",
|
||||
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",
|
||||
"jangregor/phpstan-prophecy": "^1.0",
|
||||
"jetbrains/phpstorm-attributes": "^1.0",
|
||||
@@ -73,7 +75,6 @@
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/gendoc",
|
||||
"bin/phpunit-zm",
|
||||
"bin/zhamao"
|
||||
],
|
||||
@@ -85,17 +86,6 @@
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"hooks": {
|
||||
"post-merge": "composer install",
|
||||
"pre-commit": [
|
||||
"echo committing as $(git config user.name)",
|
||||
"composer cs-fix -- --diff"
|
||||
],
|
||||
"pre-push": [
|
||||
"composer cs-fix -- --dry-run --diff",
|
||||
"composer analyse"
|
||||
]
|
||||
},
|
||||
"zm": {
|
||||
"exclude-annotation-path": [
|
||||
"src/ZM",
|
||||
@@ -104,11 +94,9 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"[ $COMPOSER_DEV_MODE -eq 0 ] || vendor/bin/cghooks add"
|
||||
],
|
||||
"post-autoload-dump": "vendor/bin/captainhook install -f -s",
|
||||
"analyse": "phpstan analyse --memory-limit 300M",
|
||||
"cs-fix": "php-cs-fixer fix",
|
||||
"cs-fix": "PHP_CS_FIXER_FUTURE_MODE=1 php-cs-fixer fix",
|
||||
"test": "bin/phpunit-zm --no-coverage"
|
||||
}
|
||||
}
|
||||
|
||||
28
config/config.php
Normal file
28
config/config.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Config 配置类的配置文件
|
||||
* 由于 Config 类是第一批被加载的类,因此本文件存在以下限制:
|
||||
* 1. 只能使用 PHP 格式
|
||||
* 2. 无法利用容器及依赖注入
|
||||
* 3. 必须存在于本地,无法使用远程配置(后续版本可能会支持)
|
||||
*/
|
||||
return [
|
||||
'repository' => [
|
||||
\OneBot\Config\Repository::class, // 配置仓库,须实现 \OneBot\Config\RepositoryInterface 接口
|
||||
[], // 传入的参数,依序传入构造函数
|
||||
],
|
||||
'loader' => [
|
||||
\OneBot\Config\Loader\DelegateLoader::class, // 配置加载器,须实现 \OneBot\Config\LoaderInterface 接口
|
||||
[], // 传入的参数,依序传入构造函数
|
||||
],
|
||||
'source' => [
|
||||
'extensions' => ['php', 'yaml', 'yml', 'json', 'toml'], // 配置文件扩展名
|
||||
'paths' => [
|
||||
SOURCE_ROOT_DIR . '/config', // 配置文件所在目录
|
||||
// 可以添加多个配置文件目录
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use OneBot\Driver\Driver;
|
||||
use OneBot\Driver\Process\ProcessManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ZM\Framework;
|
||||
|
||||
/**
|
||||
/*
|
||||
* 这里是容器的配置文件,你可以在这里配置容器的绑定和其他一些参数。
|
||||
* 选用的容器是 PHP-DI,你可以在这里查看文档:https://php-di.org/doc/
|
||||
* 我们建议你在使用容器前先阅读以下章节:
|
||||
@@ -19,8 +21,8 @@ return [
|
||||
// 这里定义的是全局容器的绑定,不建议在此处直接调用框架、应用内部的类或方法,因为这些类可能还没有被加载或初始化
|
||||
// 你可以使用匿名函数来延迟加载
|
||||
'definitions' => [
|
||||
'worker_id' => fn() => ProcessManager::getProcessId(),
|
||||
Driver::class => fn() => Framework::getInstance()->getDriver(),
|
||||
LoggerInterface::class => fn() => logger(),
|
||||
'worker_id' => fn () => ProcessManager::getProcessId(),
|
||||
Driver::class => fn () => Framework::getInstance()->getDriver(),
|
||||
LoggerInterface::class => fn () => logger(),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
---
|
||||
home: true
|
||||
heroImage: ./logo_trans.png
|
||||
actionBtn:
|
||||
text: 快速上手
|
||||
link: /guide/
|
||||
type: primary
|
||||
size: large
|
||||
actions:
|
||||
- text: 快速上手
|
||||
link: /guide/
|
||||
type: primary
|
||||
size: large
|
||||
- text: 快速上手(v2 旧版)
|
||||
link: https://docs-v2.zhamao.xin/
|
||||
type: primary
|
||||
ghost: true
|
||||
size: large
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 基于 PHP 的 Swoole 高性能扩展,利用 WebSocket 进行与 OneBot 协议兼容的聊天机器人软件的通信,还有数据库连接池、内存缓存、多任务进程等特色,大幅增强性能。
|
||||
@@ -24,7 +29,7 @@ footer: |
|
||||
此命令可一键以模板安装框架!(仅限 Linux 和 macOS)
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://zhamao.xin/go.sh)
|
||||
bash <(curl -fsSL https://zhamao.xin/v3.sh)
|
||||
```
|
||||
|
||||
## 运行框架
|
||||
|
||||
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> | 中间件参数 | [] |
|
||||
|
||||
> 关于中间件的具体用法,请参见【再来链接】
|
||||
>
|
||||
@@ -8,16 +8,13 @@
|
||||
|
||||
框架内置了对于 WebSocket 和 HTTP 的服务端和客户端支持,并针对聊天机器人消息处理进行优化扩展,提供常用会话机制和内部调用机制,让代码更为灵活。
|
||||
|
||||
这里放个代码示例
|
||||
|
||||
## 环境要求
|
||||
|
||||
虽然我们已经大力简化了运行框架的要求,但仍然存在少量的必要项:
|
||||
|
||||
- PHP 8.0 或以上版本
|
||||
- JSON 扩展
|
||||
- Tokenizer 扩展
|
||||
- Composer 工具
|
||||
- PHP 8.0 或以上版本(使用命令 `php -v` 检查)
|
||||
- Tokenizer 扩展(使用命令 `php -m | grep tokenizer` 检查)
|
||||
- Composer 工具(使用命令 `composer` 检查)
|
||||
|
||||
## 框架特色
|
||||
|
||||
@@ -25,5 +22,5 @@
|
||||
- WebSocket 服务器、HTTP 服务器兼容运行,一个框架多个用处
|
||||
- 支持命令、自然语言处理等多种插件形式
|
||||
- 支持多个机器人账号负载均衡
|
||||
- 模块分离和自由组合,可根据自身需求自己建立模块内的目录结构和代码结构
|
||||
- 完善的插件系统,可以随意加载和编写独立的插件
|
||||
- 灵活的注释和注解注册事件方式,支持 PHP 原生注解,提示更为友好
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
这里以 Walle-Q 实现端为例,在实际使用中,你可以自由选用不同的实现端。
|
||||
|
||||
你可以前往 Walle-Q 的[发布页面](https://github.com/onebot-walle/walle-q/releases)下载最新的发行版本,并运行以进行初始化。
|
||||
你可以前往 Walle-Q 的 [发布页面](https://github.com/onebot-walle/walle-q/releases) 下载最新的发行版本,并运行以进行初始化。
|
||||
|
||||
在登录成功后,请关闭 Walle-Q 以修改配置文件。
|
||||
|
||||
@@ -38,26 +38,57 @@ reconnect_interval = 4
|
||||
|
||||
修改完成并保存后,重新启动 Walle-Q 并登录即可。如果出现连接失败也请勿惊慌,因为框架此时尚未启动,失败是正常现象。
|
||||
|
||||
有些情况可能无法正常扫码登录,可使用 QQ+密码 的方式登录,在最上方插入配置:
|
||||
|
||||
```toml
|
||||
[qq.123456]
|
||||
password = "MyPassword"
|
||||
```
|
||||
|
||||
> 请将 123456 替换为你的机器人 QQ 号码,MyPassword 替换为机器人 QQ 密码。
|
||||
|
||||
## 编写第一个功能
|
||||
|
||||
在框架中,几乎所有事件的绑定都是通过注解进行的,详情可以参阅 注解的使用。
|
||||
|
||||
让我们在 `src/Module/Repeater.php` 中开发我们的第一个功能。
|
||||
让我们新建第一个插件,插件的功能很简单,就是复读。我们假设这个复读插件的名字是 `repeater`
|
||||
|
||||
```php
|
||||
namespace Module;
|
||||
|
||||
class Repeater
|
||||
{
|
||||
#[BotCommand('echo')]
|
||||
public function repeat(OneBotEvent $event, BotContext $context): void
|
||||
{
|
||||
$context->reply($event->getMessage());
|
||||
}
|
||||
}
|
||||
```bash
|
||||
./zhamao plugin:make
|
||||
# 然后根据提示,创建,比如名字输入 repeater
|
||||
# 选择类型的时候,输入 file
|
||||
```
|
||||
|
||||
借助容器的依赖注入功能,我们可以直接指定相应的类,相关实例会在调用时自动传入。
|
||||
我们就可以在目录 `plugins/repeater/` 下得到两个文件,其中 `main.php` 代码可能如下:
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$plugin = new ZMPlugin(__DIR__);
|
||||
|
||||
/*
|
||||
* 发送 "测试repeater",回复 "这是repeater插件的第一个命令!"
|
||||
*/
|
||||
$cmd1 = BotCommand::make('repeater', match: '测试repeater')->on(fn () => '这是repeater插件的第一个命令!');
|
||||
|
||||
$plugin->addBotCommand($cmd1);
|
||||
|
||||
return $plugin;
|
||||
```
|
||||
|
||||
然后根据复读的原理(简单重复一遍用户发的消息),将上方 `$cmd1` 替换为下面的指令:
|
||||
|
||||
```php
|
||||
$cmd1 = BotCommand::make('repeater', match: '复读')->on(function(OneBotEvent $event, BotContext $ctx) {
|
||||
$ctx->reply($event->getMessage());
|
||||
});
|
||||
```
|
||||
|
||||
此后,保存文件。
|
||||
|
||||
> 借助容器的依赖注入功能,我们可以直接指定相应的类,相关实例会在调用时自动传入。上方的 OneBotEvent 和 BotContext 可以自由选择位置。
|
||||
|
||||
## 启动框架
|
||||
|
||||
@@ -65,10 +96,15 @@ class Repeater
|
||||
|
||||
启动后,Walle-Q 的日志应当会显示连接成功的信息。
|
||||
|
||||
此时,你可以通过任意账号向机器人发送 `echo 给我复读` 消息,机器人会回复 `给我复读`。
|
||||
此时,你可以通过任意账号向机器人发送 `复读 给我复读` 消息,机器人会回复 `复读 给我复读`。
|
||||
|
||||
至此,你的第一个功能,复读机,也就开发完成了。
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'复读 给我复读'},
|
||||
{type:1,content:'复读 给我复读'},
|
||||
]"></chat-box>
|
||||
|
||||
## 使用机器人 API 和更多事件
|
||||
|
||||
如果你希望机器人进行其他复杂的动作(操作),请参见 机器人 API。
|
||||
|
||||
@@ -39,6 +39,9 @@ composer require zhamao/framework
|
||||
|
||||
## 安装完成后启动
|
||||
./zhamao server
|
||||
|
||||
## 生成新插件脚手架,用于开发
|
||||
./zhamao plugin:make
|
||||
```
|
||||
|
||||
## Windows 安装方法
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
# 目录结构
|
||||
|
||||
## 根目录
|
||||
## 用户目录
|
||||
|
||||
### Config 目录
|
||||
### config 目录
|
||||
|
||||
`config` 目录包含框架、应用的所有配置文件。最好把这些文件都浏览一遍,并熟悉所有可用的选项。
|
||||
|
||||
### Src 目录
|
||||
|
||||
`src` 目录包含应用的核心代码,你的大部分工作都将在这里进行。
|
||||
|
||||
### Tests 目录
|
||||
|
||||
`tests` 目录通常是你编写 PHPUnit 单元测试和功能测试的地方。你可以使用 `composer test` 运行其中的测试。
|
||||
|
||||
> 该目录并不自带
|
||||
>
|
||||
|
||||
### Vendor 目录
|
||||
|
||||
`vendor` 目录包含你通过 Composer 安装的所有依赖。
|
||||
|
||||
## Src 目录
|
||||
|
||||
你的大多数代码都位于 `src` 目录中。
|
||||
|
||||
### Globals 目录
|
||||
|
||||
`globals` 目录包含你的全局定义文件,例如全局函数和常量等。
|
||||
|
||||
需要注意的是,框架本身并不会为你自动加载其中的文件,你需要自行使用 Composer 自动加载或其他方式加载其中的代码。
|
||||
|
||||
例如 `Globals/my_functions.php` 可以被添加到 `composer.json` 当中。
|
||||
|
||||
```json
|
||||
{
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/Globals/my_functions.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
config/
|
||||
├── global.php # 全局配置文件
|
||||
├── container.php # 容器配置文件
|
||||
└── motd.txt # 框架启动时展示的文字信息
|
||||
```
|
||||
|
||||
### Module 目录
|
||||
### vendor 目录
|
||||
|
||||
`vendor` 目录包含你通过 Composer 安装的所有依赖,此目录为自动生成,无需操作。
|
||||
|
||||
### plugins 目录
|
||||
|
||||
`plugins` 目录包含你编写或加载到源代码模式的插件,里面的插件都会被框架自动扫描并解析,你可以在其中利用注解来注册事件绑定并进行相应处理。
|
||||
|
||||
比如你通过 `./zhamao plugin:make` 新建了一个名字叫 `test-app` 的插件,并且设置为单文件模式(`file`),那么这个插件内包含的文件及结构为:
|
||||
|
||||
```
|
||||
plugins/
|
||||
└── test-app/
|
||||
├── main.php # 你的插件源代码文件
|
||||
└── zmplugin.json # 插件元信息(如名称、版本等)
|
||||
```
|
||||
|
||||
### zm_data 目录
|
||||
|
||||
`zm_data` 目录存放了框架运行时持久化保存的数据,例如 KV 数据库、驱动日志等内容。
|
||||
|
||||
`module` 目录包含你机器人或是服务的主体代码,其中的所有类都会被框架自动扫描并解析,你可以在其中利用注解来注册事件绑定并进行相应处理。
|
||||
|
||||
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,18 @@ 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 = ''): Psr\SimpleCache\CacheInterface
|
||||
{
|
||||
global $kv_class;
|
||||
if (!$kv_class) {
|
||||
$kv_class = config('global.kv.use', \LightCache::class);
|
||||
}
|
||||
/* @phpstan-ignore-next-line */
|
||||
return is_a($kv_class, KVInterface::class, true) ? $kv_class::open($name) : new $kv_class($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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,15 +94,6 @@ class AnnotationHandler
|
||||
foreach ((AnnotationMap::$_list[$this->annotation_class] ?? []) as $v) {
|
||||
// 调用单个注解
|
||||
$this->handle($v, $this->rule_callback, ...$params);
|
||||
// 执行完毕后检查状态,如果状态是规则判断或中间件before不通过,则重置状态后继续执行别的注解函数
|
||||
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) {
|
||||
$this->status = self::STATUS_NORMAL;
|
||||
continue;
|
||||
}
|
||||
// 如果执行完毕,且设置了返回值后续逻辑的回调函数,那么就调用返回值回调的逻辑
|
||||
if (is_callable($this->return_callback) && $this->status === self::STATUS_NORMAL) {
|
||||
($this->return_callback)($this->return_val);
|
||||
}
|
||||
}
|
||||
} catch (InterruptException $e) {
|
||||
// InterruptException 用于中断,这里必须 catch,并标记状态
|
||||
@@ -140,6 +131,15 @@ class AnnotationHandler
|
||||
}
|
||||
try {
|
||||
$this->return_val = middleware()->process($callback, ...$args);
|
||||
// 执行完毕后检查状态,如果状态是规则判断或中间件before不通过,则重置状态后继续执行别的注解函数
|
||||
if ($this->status == self::STATUS_BEFORE_FAILED || $this->status == self::STATUS_RULE_FAILED) {
|
||||
$this->status = self::STATUS_NORMAL;
|
||||
return false;
|
||||
}
|
||||
// 如果执行完毕,且设置了返回值后续逻辑的回调函数,那么就调用返回值回调的逻辑
|
||||
if (is_callable($this->return_callback) && $this->status === self::STATUS_NORMAL) {
|
||||
($this->return_callback)($this->return_val);
|
||||
}
|
||||
} catch (InterruptException $e) {
|
||||
// 这里直接抛出这个异常的目的就是给上层handleAll()捕获
|
||||
throw $e;
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace ZM\Config;
|
||||
|
||||
use OneBot\Config\Config;
|
||||
use OneBot\Config\Loader\LoaderInterface;
|
||||
use OneBot\Util\Singleton;
|
||||
use ZM\Exception\ConfigException;
|
||||
use ZM\Framework;
|
||||
@@ -13,21 +14,11 @@ class ZMConfig
|
||||
{
|
||||
use Singleton;
|
||||
|
||||
/**
|
||||
* @var array 支持的文件扩展名
|
||||
*/
|
||||
public const ALLOWED_FILE_EXTENSIONS = ['php', 'yaml', 'yml', 'json', 'toml'];
|
||||
|
||||
/**
|
||||
* @var array 配置文件加载顺序,后覆盖前
|
||||
*/
|
||||
public const LOAD_ORDER = ['default', 'environment', 'patch'];
|
||||
|
||||
/**
|
||||
* @var string 默认配置文件路径
|
||||
*/
|
||||
public const DEFAULT_CONFIG_PATH = SOURCE_ROOT_DIR . '/config';
|
||||
|
||||
/**
|
||||
* @var string[] 环境别名
|
||||
*/
|
||||
@@ -42,6 +33,11 @@ class ZMConfig
|
||||
*/
|
||||
private array $loaded_files = [];
|
||||
|
||||
/**
|
||||
* @var array 配置文件扩展名
|
||||
*/
|
||||
private array $file_extensions = [];
|
||||
|
||||
/**
|
||||
* @var array 配置文件路径
|
||||
*/
|
||||
@@ -62,24 +58,42 @@ class ZMConfig
|
||||
*/
|
||||
private ?ConfigTracer $tracer = null;
|
||||
|
||||
/**
|
||||
* @var LoaderInterface 配置加载器
|
||||
* @phpstan-ignore-next-line We will use this property in the future.
|
||||
*/
|
||||
private LoaderInterface $loader;
|
||||
|
||||
/**
|
||||
* 构造配置实例
|
||||
*
|
||||
* @param array $config_paths 配置文件路径
|
||||
* @param string $environment 环境
|
||||
* @param string $environment 环境
|
||||
*
|
||||
* @throws ConfigException 配置文件加载出错
|
||||
*/
|
||||
public function __construct(array $config_paths = [], string $environment = 'uninitiated')
|
||||
public function __construct(string $environment = 'uninitiated', array $init_config = null)
|
||||
{
|
||||
$this->config_paths = $config_paths ?: [self::DEFAULT_CONFIG_PATH];
|
||||
$conf = $init_config ?: $this->loadInitConfig();
|
||||
$this->file_extensions = $conf['source']['extensions'];
|
||||
$this->config_paths = $conf['source']['paths'];
|
||||
|
||||
$this->environment = self::$environment_alias[$environment] ?? $environment;
|
||||
$this->holder = new Config([]);
|
||||
|
||||
// 初始化配置容器
|
||||
$this->holder = new Config(
|
||||
new ($conf['repository'][0])(...$conf['repository'][1]),
|
||||
);
|
||||
|
||||
// 初始化配置加载器
|
||||
$this->loader = new ($conf['loader'][0])(...$conf['loader'][1]);
|
||||
|
||||
// 调试模式下启用配置跟踪器
|
||||
if (Framework::getInstance()->getArgv()['debug'] ?? false) {
|
||||
$this->tracer = new ConfigTracer();
|
||||
} else {
|
||||
$this->tracer = null;
|
||||
}
|
||||
|
||||
if ($environment !== 'uninitiated') {
|
||||
$this->loadFiles();
|
||||
}
|
||||
@@ -104,7 +118,7 @@ class ZMConfig
|
||||
foreach ($files as $file) {
|
||||
[, $ext, $load_type] = $this->getFileMeta($file);
|
||||
// 略过不支持的文件
|
||||
if (!in_array($ext, self::ALLOWED_FILE_EXTENSIONS, true)) {
|
||||
if (!in_array($ext, $this->file_extensions, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -347,12 +361,14 @@ class ZMConfig
|
||||
|
||||
// 判断文件格式是否支持
|
||||
[$group, $ext, $load_type, $env] = $this->getFileMeta($path);
|
||||
if (!in_array($ext, self::ALLOWED_FILE_EXTENSIONS, true)) {
|
||||
if (!in_array($ext, $this->file_extensions, true)) {
|
||||
throw ConfigException::unsupportedFileType($path);
|
||||
}
|
||||
|
||||
// 读取并解析配置
|
||||
$content = file_get_contents($path);
|
||||
// TODO: 使用 Loader 替代
|
||||
// $config = $this->loader->load($path);
|
||||
$config = [];
|
||||
switch ($ext) {
|
||||
case 'php':
|
||||
@@ -396,8 +412,11 @@ class ZMConfig
|
||||
$this->merge($group, $config);
|
||||
logger()->debug("已载入配置文件:{$path}");
|
||||
|
||||
if ($this->tracer !== null) {
|
||||
$this->tracer->addTracesOf($group, $config, $path);
|
||||
}
|
||||
$this->tracer?->addTracesOf($group, $config, $path);
|
||||
}
|
||||
|
||||
private function loadInitConfig(): array
|
||||
{
|
||||
return require SOURCE_ROOT_DIR . '/config/config.php';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 $bots = [];
|
||||
|
||||
private static array $echo_id_list = [];
|
||||
|
||||
private array $self;
|
||||
@@ -28,9 +27,11 @@ 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];
|
||||
self::$bots[$bot_id][$platform] = $this;
|
||||
$this->base_event = $event;
|
||||
}
|
||||
|
||||
public function getEvent(): OneBotEvent
|
||||
@@ -41,11 +42,7 @@ class BotContext implements ContextInterface
|
||||
/**
|
||||
* 快速回复机器人消息文本
|
||||
*
|
||||
* @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws OneBot12Exception
|
||||
* @throws \Throwable
|
||||
* @param array|MessageSegment|string|\Stringable $message 消息内容、消息段或消息段数组
|
||||
*/
|
||||
public function reply(\Stringable|MessageSegment|array|string $message)
|
||||
{
|
||||
@@ -76,25 +73,24 @@ class BotContext implements ContextInterface
|
||||
/**
|
||||
* 获取其他机器人的上下文操作对象
|
||||
*
|
||||
* @param string $bot_id 机器人的 self.user_id 对应的 ID
|
||||
* @param string $platform 机器人的 self.platform 对应的 platform
|
||||
* @return $this
|
||||
* @param string $bot_id 机器人的 self.user_id 对应的 ID
|
||||
* @param string $platform 机器人的 self.platform 对应的 platform
|
||||
* @throws OneBot12Exception
|
||||
*/
|
||||
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);
|
||||
if (isset(self::$bots[$bot_id])) {
|
||||
if ($platform === '') {
|
||||
$one = current(self::$bots[$bot_id]);
|
||||
if ($one instanceof BotContext) {
|
||||
return $one;
|
||||
}
|
||||
} elseif (isset(self::$bots[$bot_id][$platform])) {
|
||||
return self::$bots[$bot_id][$platform];
|
||||
}
|
||||
}
|
||||
// 到这里说明没找到对应的机器人,抛出异常
|
||||
throw new OneBot12Exception('Bot not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,38 +131,8 @@ class BotContext implements ContextInterface
|
||||
return self::$echo_id_list[$echo] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function sendAction(string $action, array $params = [], ?array $self = null)
|
||||
public function getSelf(): array
|
||||
{
|
||||
// 声明 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.');
|
||||
return $this->self;
|
||||
}
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
@@ -45,23 +45,23 @@ class HttpEventListener
|
||||
$result = HttpUtil::parseUri($event->getRequest(), $node, $params);
|
||||
switch ($result) {
|
||||
case ZM_ERR_NONE: // 解析到存在路由了
|
||||
$handler = new AnnotationHandler(Route::class);
|
||||
$route_handler = new AnnotationHandler(Route::class);
|
||||
$div = new Route($node['route']);
|
||||
$div->params = $params;
|
||||
$div->method = $node['method'];
|
||||
// TODO:这里有个bug,逻辑上 request_method 应该是个数组,而不是字符串,但是这里 $node['method'] 是字符串,所以这里只能用字符串来判断
|
||||
// $div->request_method = $node['request_method'];
|
||||
$div->class = $node['class'];
|
||||
$starttime = microtime(true);
|
||||
$handler->handle($div, null, $params, $event->getRequest(), $event);
|
||||
if (is_string($val = $handler->getReturnVal()) || ($val instanceof \Stringable)) {
|
||||
$route_handler->handle($div, null, $params, $event->getRequest(), $event);
|
||||
if (is_string($val = $route_handler->getReturnVal()) || ($val instanceof \Stringable)) {
|
||||
// 返回的内容是可以被字符串化的,就当作 Body 来返回,状态码 200
|
||||
$event->withResponse(HttpFactory::createResponse(200, null, [], Stream::create($val)));
|
||||
} elseif ($event->getResponse() === null) {
|
||||
// 过了一遍 Route,没有促成 Response,则返回 500(路由必须有返回才行)
|
||||
$event->withResponse(HttpFactory::createResponse(500));
|
||||
}
|
||||
logger()->warning('Used ' . round((microtime(true) - $starttime) * 1000, 3) . ' ms');
|
||||
break;
|
||||
case ZM_ERR_ROUTE_METHOD_NOT_ALLOWED:
|
||||
case ZM_ERR_ROUTE_METHOD_NOT_ALLOWED: // 路由检测到存在,但是方法不匹配,则返回 405,表示方法不受支持
|
||||
$event->withResponse(HttpUtil::handleHttpCodePage(405));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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 = 656;
|
||||
|
||||
/** @var string 版本名称 */
|
||||
public const VERSION = '3.0.0-beta2';
|
||||
public const VERSION = '3.0.0-beta4';
|
||||
|
||||
/** @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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -365,12 +365,12 @@ class OneBot12Adapter extends ZMPlugin
|
||||
continue;
|
||||
}
|
||||
// 测试 match
|
||||
if ($v->match !== '' && $v->match === $head) {
|
||||
if ($v->match !== '' && ($v->prefix . $v->match) === $head) {
|
||||
array_shift($cmd_explode);
|
||||
return [$v, $cmd_explode, $full_str];
|
||||
}
|
||||
// 测试 alias
|
||||
if ($v->match !== '' && $v->alias !== [] && in_array($head, $v->alias, true)) {
|
||||
if ($v->match !== '' && $v->alias !== [] && in_array($head, array_map(fn ($x) => $v->prefix . $x, $v->alias), true)) {
|
||||
array_shift($cmd_explode);
|
||||
return [$v, $cmd_explode, $full_str];
|
||||
}
|
||||
@@ -610,7 +610,7 @@ class OneBot12Adapter extends ZMPlugin
|
||||
{
|
||||
$handler = new AnnotationHandler(BotCommand::class);
|
||||
$handler->setReturnCallback(function ($result) use ($ctx) {
|
||||
if (is_string($result)) {
|
||||
if (is_string($result) || $result instanceof MessageSegment) {
|
||||
$ctx->reply($result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
12
src/ZM/Store/KV/KVInterface.php
Normal file
12
src/ZM/Store/KV/KVInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
interface KVInterface
|
||||
{
|
||||
public static function open(string $name = ''): CacheInterface;
|
||||
}
|
||||
191
src/ZM/Store/KV/LightCache.php
Normal file
191
src/ZM/Store/KV/LightCache.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ZM\Store\KV;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use ZM\Exception\InvalidArgumentException;
|
||||
use ZM\Process\ProcessStateManager;
|
||||
use ZM\Store\FileSystem;
|
||||
|
||||
/**
|
||||
* 轻量、基于本地 JSON 文件的 KV 键值对缓存
|
||||
*/
|
||||
class LightCache implements CacheInterface, 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 = ''): CacheInterface
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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, null|int|\DateInterval $ttl = null): bool
|
||||
{
|
||||
$this->validateKey($key);
|
||||
self::$caches[$this->name][$key] = $value;
|
||||
if ($ttl !== null) {
|
||||
if ($ttl instanceof \DateInterval) {
|
||||
$ttl = $ttl->days * 86400 + $ttl->h * 3600 + $ttl->i * 60 + $ttl->s;
|
||||
}
|
||||
self::$ttys[$this->name][$key] = time() + $ttl;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(string $key): bool
|
||||
{
|
||||
unset(self::$caches[$this->name][$key], self::$ttys[$this->name][$key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function clear(): 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 getMultiple(iterable $keys, mixed $default = null): iterable
|
||||
{
|
||||
foreach ($keys as $v) {
|
||||
yield $v => $this->get($v, $default);
|
||||
}
|
||||
}
|
||||
|
||||
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool
|
||||
{
|
||||
foreach ($values as $k => $v) {
|
||||
if (!$this->set($k, $v, $ttl)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteMultiple(iterable $keys): bool
|
||||
{
|
||||
foreach ($keys as $v) {
|
||||
if (!$this->delete($v)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function has(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) {
|
||||
|
||||
@@ -56,9 +56,9 @@ class ZMConfigTest extends TestCase
|
||||
]);
|
||||
|
||||
try {
|
||||
$config = new ZMConfig([
|
||||
$this->vfs->url(),
|
||||
], 'development');
|
||||
$init_conf = require SOURCE_ROOT_DIR . '/config/config.php';
|
||||
$init_conf['source']['paths'] = [$this->vfs->url()];
|
||||
$config = new ZMConfig('development', $init_conf);
|
||||
} catch (ConfigException $e) {
|
||||
$this->fail($e->getMessage());
|
||||
}
|
||||
|
||||
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->clear());
|
||||
}
|
||||
|
||||
public function testSet()
|
||||
{
|
||||
$this->assertTrue(LightCache::open()->set('test123', 'help'));
|
||||
}
|
||||
|
||||
public function testIsset()
|
||||
{
|
||||
$this->assertFalse(LightCache::open()->has('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->delete('test');
|
||||
$this->assertNull($kv->get('test'));
|
||||
}
|
||||
}
|
||||
32
tests/ZM/Utils/ZMRequestTest.php
Normal file
32
tests/ZM/Utils/ZMRequestTest.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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://httpbin.org/get', [
|
||||
'X-Test' => '123',
|
||||
]);
|
||||
$this->assertStringContainsString('123', $r);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user