Compare commits

...

30 Commits

Author SHA1 Message Date
Jerry
5cd28964e6 Merge pull request #225 from zhamao-robot/refactor-config-config
重构配置类配置
2022-12-31 21:04:28 +08:00
crazywhalecc
d87b7dc0f7 increment build number (build 656) 2022-12-31 12:55:21 +00:00
Jerry
1d27091558 Merge pull request #224 from zhamao-robot/edge-bug-fix
边缘 Bug 修复
2022-12-31 20:53:35 +08:00
crazywhalecc
3dafbf4fbd increment build number (build 655) 2022-12-31 12:50:11 +00:00
Jerry
3a78f5e2b1 Merge pull request #223 from zhamao-robot/fix-ob12-cmd-bug
修复有关 BotCommand 的 Bug
2022-12-31 20:29:16 +08:00
sunxyw
3f26648a3c fix ZMRequestTest wrong assertion 2022-12-31 20:22:55 +08:00
sunxyw
a62e950870 add more path to cs-fix 2022-12-31 20:12:45 +08:00
sunxyw
aee7fa332a refactor config config 2022-12-31 20:12:20 +08:00
crazywhalecc
c1a0fae6e6 add interactive chat box docs 2022-12-31 19:57:49 +08:00
crazywhalecc
ed31edcc5c add some comments 2022-12-31 19:39:50 +08:00
crazywhalecc
4aea90cb39 add multiple bot support 2022-12-31 19:39:08 +08:00
crazywhalecc
a58c3aadab remove debug logger 2022-12-31 19:27:06 +08:00
Jerry
f2fb40b67c Merge pull request #222 from zhamao-robot/psr-simple-cache
将 KV 库接口调整为 PSR-16
2022-12-31 19:25:00 +08:00
crazywhalecc
18df76f650 fix AnnotationHandler return callback not working for BotCommand 2022-12-31 19:12:29 +08:00
crazywhalecc
dce126136b fix BotCommand prefix bug 2022-12-31 19:11:39 +08:00
crazywhalecc
d32f7b0ff8 update to beta4 version 2022-12-31 19:10:50 +08:00
crazywhalecc
d55362e190 update global functions for KV 2022-12-31 19:08:52 +08:00
crazywhalecc
6bcedea720 cs fix 2022-12-31 19:04:57 +08:00
Jerry
ccc801e6cb Merge pull request #221 from zhamao-robot/docs-update-1
更新部分文档
2022-12-31 19:03:07 +08:00
crazywhalecc
a2404482a3 change to psr-16 2022-12-31 19:02:41 +08:00
crazywhalecc
4c41dd09d2 update some docs 2022-12-31 18:12:28 +08:00
crazywhalecc
ab83194bbe Merge remote-tracking branch 'origin/main' 2022-12-31 17:42:18 +08:00
crazywhalecc
6013571267 update README.md and docs test activate 2022-12-31 17:41:20 +08:00
Jerry
2636bc2e35 Merge pull request #220 from zhamao-robot/update-workflows
根据最新的分支命名更新 Workflows
2022-12-31 17:37:01 +08:00
crazywhalecc
a2b013402b update workflows for newest 3.0 branch 2022-12-31 17:29:26 +08:00
Jerry
206f041d29 Merge pull request #219 from zhamao-robot/update-v3-docs-build
升级 v3 文档构建发布路径
2022-12-31 17:20:18 +08:00
crazywhalecc
ce885a7a61 update 3.0 docs build path 2022-12-31 17:19:23 +08:00
crazywhalecc
b0d0d5eba9 update 3.0 docs build path 2022-12-31 17:19:05 +08:00
sunxyw
d45c4e24fd switch hook to captainhook (#218) 2022-12-31 17:17:03 +08:00
crazywhalecc
49fffcc464 update 3.0 docs build path 2022-12-31 17:15:18 +08:00
26 changed files with 345 additions and 226 deletions

View File

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

View File

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

3
.gitignore vendored
View File

@@ -82,3 +82,6 @@ package-lock.json
/.tool-version
.DS_Store
### PHP CS Fixer ###
.php-cs-fixer.cache

View File

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

View File

@@ -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) 进行疑难解答。

47
captainhook.json Normal file
View 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"
]
]
}
]
}
]
}
}

View File

@@ -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",
@@ -84,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",
@@ -103,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
View 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', // 配置文件所在目录
// 可以添加多个配置文件目录
],
],
];

View File

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

View File

@@ -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)
```
## 运行框架

View File

@@ -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 原生注解,提示更为友好

View File

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

View File

@@ -39,6 +39,9 @@ composer require zhamao/framework
## 安装完成后启动
./zhamao server
## 生成新插件脚手架,用于开发
./zhamao plugin:make
```
## Windows 安装方法

View File

@@ -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` 目录包含你机器人或是服务的主体代码,其中的所有类都会被框架自动扫描并解析,你可以在其中利用注解来注册事件绑定并进行相应处理。

View File

@@ -248,11 +248,12 @@ function bot(): ZM\Context\BotContext
return new \ZM\Context\BotContext('', '');
}
function kv(string $name = ''): KVInterface
function kv(string $name = ''): Psr\SimpleCache\CacheInterface
{
global $kv_class;
if (!$kv_class) {
$kv_class = config('global.kv.use', \LightCache::class);
}
return $kv_class::open($name);
/* @phpstan-ignore-next-line */
return is_a($kv_class, KVInterface::class, true) ? $kv_class::open($name) : new $kv_class($name);
}

View File

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

View 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';
}
}

View File

@@ -9,8 +9,6 @@ use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
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\Context\Trait\BotActionTrait;
use ZM\Exception\OneBot12Exception;
use ZM\Utils\MessageUtil;
@@ -19,6 +17,8 @@ class BotContext implements ContextInterface
{
use BotActionTrait;
private static array $bots = [];
private static array $echo_id_list = [];
private array $self;
@@ -30,6 +30,7 @@ class BotContext implements ContextInterface
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;
}
@@ -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,13 +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
{
return $this;
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.');
}
/**
@@ -122,4 +130,9 @@ class BotContext implements ContextInterface
{
return self::$echo_id_list[$echo] ?? null;
}
public function getSelf(): array
{
return $this->self;
}
}

View File

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

View File

@@ -43,10 +43,10 @@ class Framework
use Singleton;
/** @var int 版本ID */
public const VERSION_ID = 654;
public const VERSION_ID = 656;
/** @var string 版本名称 */
public const VERSION = '3.0.0-beta3';
public const VERSION = '3.0.0-beta4';
/** @var array 传入的参数 */
protected array $argv;

View File

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

View File

@@ -4,44 +4,9 @@ declare(strict_types=1);
namespace ZM\Store\KV;
use Psr\SimpleCache\CacheInterface;
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;
public static function open(string $name = ''): CacheInterface;
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ZM\Store\KV;
use Psr\SimpleCache\CacheInterface;
use ZM\Exception\InvalidArgumentException;
use ZM\Process\ProcessStateManager;
use ZM\Store\FileSystem;
@@ -11,7 +12,7 @@ use ZM\Store\FileSystem;
/**
* 轻量、基于本地 JSON 文件的 KV 键值对缓存
*/
class LightCache implements KVInterface
class LightCache implements CacheInterface, KVInterface
{
/** @var array 存放库对象的列表 */
private static array $objs = [];
@@ -49,7 +50,7 @@ class LightCache implements KVInterface
/**
* @throws InvalidArgumentException
*/
public static function open(string $name = ''): KVInterface
public static function open(string $name = ''): CacheInterface
{
if (!isset(self::$objs[$name])) {
self::$objs[$name] = new LightCache($name);
@@ -82,18 +83,6 @@ class LightCache implements KVInterface
], 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
{
// 首先判断在不在缓存变量里
@@ -115,24 +104,62 @@ class LightCache implements KVInterface
/**
* @throws InvalidArgumentException
*/
public function set(string $key, mixed $value, int $ttl = 0): bool
public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool
{
$this->validateKey($key);
self::$caches[$this->name][$key] = $value;
if ($ttl > 0) {
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 unset(string $key): bool
public function delete(string $key): bool
{
unset(self::$caches[$this->name][$key], self::$ttys[$this->name][$key]);
return true;
}
public function isset(string $key): bool
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;

View File

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

View File

@@ -17,7 +17,7 @@ class LightCacheTest extends TestCase
$a = LightCache::open('asd');
$this->assertInstanceOf(LightCache::class, $a);
/* @phpstan-ignore-next-line */
$this->assertTrue($a->removeSelf());
$this->assertTrue($a->clear());
}
public function testSet()
@@ -27,7 +27,7 @@ class LightCacheTest extends TestCase
public function testIsset()
{
$this->assertFalse(LightCache::open()->isset('test111'));
$this->assertFalse(LightCache::open()->has('test111'));
}
public function testGet()
@@ -41,7 +41,7 @@ class LightCacheTest extends TestCase
$kv = LightCache::open('sss');
$kv->set('test', 'test');
$this->assertSame($kv->get('test'), 'test');
$kv->unset('test');
$kv->delete('test');
$this->assertNull($kv->get('test'));
}
}

View File

@@ -24,7 +24,9 @@ class ZMRequestTest extends TestCase
public function testGet()
{
$r = ZMRequest::get('http://ip.zhamao.xin');
$this->assertStringContainsString('114', $r);
$r = ZMRequest::get('http://httpbin.org/get', [
'X-Test' => '123',
]);
$this->assertStringContainsString('123', $r);
}
}