mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-07-06 00:05:36 +08:00
Compare commits
30 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 |
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
|
||||
|
||||
4
.github/workflows/vuepress-deploy.yml
vendored
4
.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:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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) 进行疑难解答。
|
||||
|
||||
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",
|
||||
@@ -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
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(),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
## 运行框架
|
||||
|
||||
@@ -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` 目录包含你机器人或是服务的主体代码,其中的所有类都会被框架自动扫描并解析,你可以在其中利用注解来注册事件绑定并进行相应处理。
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user