Compare commits

..

1 Commits

Author SHA1 Message Date
Whale
c867eac993 Update README.md 2020-12-09 16:37:55 +08:00
325 changed files with 8027 additions and 16925 deletions

View File

@@ -1,15 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2

5
.entry.sh Normal file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
if [ ! -d "/app/zhamao-framework/bin" ]; then
cp -r /app/zhamao-framework-bak/* /app/zhamao-framework/
fi
php /app/zhamao-framework/bin/start

21
.gitattributes vendored
View File

@@ -1,21 +0,0 @@
# Exclude build/test files from archive (reduce package size)
/.github export-ignore
/.phive export-ignore
/build export-ignore
/docs export-ignore
/ext export-ignore
/resources export-ignore
/tests export-ignore
/tests_old export-ignore
/.editorconfig export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php-cs-fixer.php export-ignore
/captainhook.json export-ignore
/install-runtime.sh export-ignore
/package.json export-ignore
/phpstan.neon export-ignore
/phpunit.xml.dist export-ignore
# Configure diff output
*.php diff=php

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
name: Code Style
on:
push:
pull_request:
workflow_dispatch:
jobs:
pre_job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: same_content_newer
skip_after_successful_duplicate: true
paths: '["src/**", "tests/**"]'
do_not_skip: '["workflow_dispatch", "schedule"]'
cs-fix:
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
name: Code Style
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PHP
uses: sunxyw/workflows/setup-environment@main
with:
php-version: 8.0
php-extensions: swoole, posix, json
operating-system: ubuntu-latest
use-cache: true
- name: Code Style
uses: sunxyw/workflows/coding-style@main

View File

@@ -1,61 +0,0 @@
name: Increment Build Number
on:
pull_request:
branches:
- main
types:
- closed
paths:
- 'src/**.php'
jobs:
incremental-build-number:
if: github.event.pull_request.merged == true
name: Incremental Build Number
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
ref: ${{ github.base_ref }}
- name: Setup PHP
uses: sunxyw/workflows/setup-environment@main
with:
php-version: 8.1
php-extensions: swoole, posix, json
operating-system: ubuntu-latest
use-cache: true
- name: Prepare Doxygen
id: prepare-doxygen
continue-on-error: true
run: bin/prepare-doxygen before
- name: Generate Doxygen
if: steps.prepare-doxygen.outcome == 'success'
uses: mattnotmitt/doxygen-action@v1.9.5
- name: Finishing Doxygen
if: steps.prepare-doxygen.outcome == 'success'
run: bin/prepare-doxygen after
- name: Commit API Docs
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: update api docs
branch: ${{ github.base_ref }}
- name: Increment build number
id: increment-build-number
run: |
sed -i -r 's/(.*)(\VERSION_ID\s=\s)([0-9]+)(.*)/echo "\1\2$((\3+1))\4"/ge' src/ZM/Framework.php
BUILD_ID=$(cat src/ZM/Framework.php | grep "VERSION_ID = " | sed 's/[^0-9]//g')
echo "::set-output name=build_id::$BUILD_ID"
- name: Commit build number
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: increment build number (build ${{ steps.increment-build-number.outputs.build_id }})
branch: ${{ github.base_ref }}

View File

@@ -1,43 +0,0 @@
name: Static Analysis
on:
push:
pull_request:
workflow_dispatch:
jobs:
pre_job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: same_content_newer
skip_after_successful_duplicate: true
paths: '["src/**", "tests/**"]'
do_not_skip: '["workflow_dispatch", "schedule"]'
analyse:
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
name: Static Analysis
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PHP
uses: sunxyw/workflows/setup-environment@main
with:
php-version: 8.0
php-extensions: swoole, posix, json
operating-system: ubuntu-latest
use-cache: true
- name: Static Analysis
uses: sunxyw/workflows/static-analysis@main

View File

@@ -1,55 +0,0 @@
name: Test
on:
push:
branches:
- main
- develop
- '*-dev*'
pull_request:
types:
- opened
- reopened
- review_requested
workflow_dispatch:
jobs:
pre_job:
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: same_content_newer
skip_after_successful_duplicate: true
paths: '["src/**", "tests/**", "bin/phpunit-zm"]'
do_not_skip: '["workflow_dispatch", "schedule"]'
test:
needs: pre_job
if: ${{ needs.pre_job.outputs.should_skip != 'true' }}
strategy:
matrix:
operating-system: [ ubuntu-latest, windows-latest, macos-latest ]
php-version: [ 8.0, 8.1, 8.2 ]
name: PHP ${{ matrix.php-version }} Test (${{ matrix.operating-system }})
runs-on: ${{ matrix.operating-system }}
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PHP
uses: sunxyw/workflows/setup-environment@main
with:
php-version: ${{ matrix.php-version }}
php-extensions: swoole, posix, json
operating-system: ${{ matrix.operating-system }}
use-cache: true
- name: Test
uses: sunxyw/workflows/test@main

View File

@@ -1,51 +0,0 @@
name: Docs and Script Auto Deploy
on:
push:
branches:
- main
paths:
- 'docs/**'
- 'ext/**'
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v3
- name: Deploy docs to GitHub Pages
uses: jenkey2011/vuepress-deploy@master
env:
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_SCRIPT: yarn && yarn docs:build
BUILD_DIR: docs/.vuepress/dist/
- name: Copy deployment to current folder
run: |
cp -r "${GITHUB_WORKSPACE}/docs/.vuepress/dist" "./deploy"
- name: Deploy to Zhamao Server
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.ZHAMAO_XIN_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "deploy/"
REMOTE_HOST: ${{ secrets.ZHAMAO_XIN_HOST }}
REMOTE_USER: ${{ secrets.ZHAMAO_XIN_USER }}
TARGET: ${{ secrets.FRAMEWORK_ZHAMAO_XIN_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/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 }}

91
.gitignore vendored
View File

@@ -1,87 +1,8 @@
### Zhamao Framework ###
/zm_data/
/resources/server.phar
/distribute/
/resources/zhamao.service
/runtime/
/tmp/
/temp/
/site/
/plugins/
/doxy/
/walle/
# 框架审计文件
audit.log
# 进程锁文件
.daemon_pid
.zm_worker_*.pid
### Composer ###
composer.phar
.idea/
/src/test/
/src/webconsole/config/
/vendor/
zm.json
/zm_data/
composer.lock
### Go CQHTTP ###
/ext/go-cqhttp/data/
/ext/go-cqhttp/logs/
/ext/go-cqhttp/config.hjson
/ext/go-cqhttp/device.json
/ext/go-cqhttp/go-cqhttp
/ext/go-cqhttp/session.token
### Git ###
# 备份文件
*.orig
# 解决 Merge 冲突时可能会自动生成的文件
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
# Git Hooks
cghooks.lock
### VuePress ###
/node_modules/
/docs/.vuepress/dist/
package-lock.json
### PHPUnit ###
# 生成的文件
/bin/.phpunit.result.cache
.phpunit.result.cache
.phpunit.cache
# 本地配置
/phpunit.xml
# 构建目录
/build/
### PhpStorm ###
# 兼容 PHPStorm 及其他 Jetbrains IDE
# 除了代码格式配置和运行配置之外的所有文件
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Phive ###
/.phive/
/tools/
### ASDF ###
/.tool-version
.DS_Store
### PHP CS Fixer ###
.php-cs-fixer.cache
/resources/server.phar

View File

@@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
/**
* @since 2.7.0
*/
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
'@Symfony' => true,
'@PhpCsFixer' => true,
'array_syntax' => [
'syntax' => 'short',
],
'list_syntax' => [
'syntax' => 'short',
],
'concat_space' => [
'spacing' => 'one',
],
'blank_line_before_statement' => [
'statements' => [
'declare',
],
],
'ordered_imports' => [
'imports_order' => [
'class',
'function',
'const',
],
'sort_algorithm' => 'alpha',
],
'single_line_comment_style' => [
'comment_types' => [
],
],
'yoda_style' => [
'always_move_variable' => false,
'equal' => false,
'identical' => false,
],
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'constant_case' => [
'case' => 'lower',
],
'class_attributes_separation' => true,
'combine_consecutive_unsets' => true,
'declare_strict_types' => true,
'linebreak_after_opening_tag' => true,
'lowercase_static_reference' => true,
'no_useless_else' => true,
'no_unused_imports' => true,
'not_operator_with_successor_space' => false,
'not_operator_with_space' => false,
'ordered_class_elements' => true,
'php_unit_strict' => false,
'phpdoc_separation' => false,
'single_quote' => true,
'standardize_not_equals' => true,
'multiline_comment_opening_closing' => true,
'phpdoc_summary' => false,
'php_unit_test_class_requires_covers' => false,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__ . '/src')
->in(__DIR__ . '/tests')
->in(__DIR__ . '/config')
->in(__DIR__ . '/bin')
);

View File

@@ -1,11 +0,0 @@
<?php
namespace PHPSTORM_META {
use OneBot\V12\Object\Event\OneBotEvent;
use ZM\Context\Context;
override(Context::__call(0), map([
'getBotEvent' => OneBotEvent::class,
]));
}

View File

@@ -1,68 +0,0 @@
# 炸毛框架贡献指南
首先很高兴你愿意读这篇文章,因为我们需要更多的开发者来帮助我们开发和维护。
如果你还没加入我们的 [QQ 群670821194][qq-group],我们建议你加入一下,以便和其他开发者实时交流和协作。
## 测试
我们目前采用 PHPUnit 作为测试框架,如果你编写了新的功能或改变了现有的行为,我们希望你可以编写相应的测试代码。
## 提交变更
请使用 [GitHub Pull Request][new-pr] 提交你的更改,并附上恰当的说明(例如一个简明的列表列出所作的改动)。
同时,请不要忘记在提交前执行代码格式检查 `composer cs-fix` 和静态分析检查 `composer analyse`
。虽然我们的自动化流程会在你推送到仓库后自动运行检查,但我仍然建议你提前在本地运行,以节省可能的修改成本。
对了,请尽量为你的每个函数、方法加上 PHPDoc 注释。
## 具体贡献流程
#### **你发现了个 BUG**
- **如果你发现的是安全漏洞,请不要在 GitHub 上回报!** 请直接联系 [@crazywhalecc](https://github.com/crazywhalecc)。
- **确保该问题还没被其他人回报。** 你可以在 [Issue 列表][issues] 中搜索相关关键词来检查。
- 如果你没能找到一个已有的 Issue 来描述你遇到的问题,[请新建一个 Issue][new-issue]。请务必包含**标题和清晰的描述**
,尽可能多的相关信息,以及一个**代码示例**或者一个**可执行的测试用例**,来描述你遇到的问题。
- 如果可以的话,请使用相关的 Issue 模板来创建 Issue。
#### **你修复了个 BUG**
- 在 GitHub 上提交一个 Pull Request。
- 确保 PR 的描述清楚地描述了问题和解决方案。如果有相关的 Issue请在 PR 描述中包含 Issue 编号。
#### **你想添加新功能或改变现有功能?**
- 虽然你可以直接提交 PR但我们建议你先与我们的开发者讨论你的想法以避免你的努力白费。你可以在 [QQ 群][qq-group]
中与我们的开发者交流,或者在 [GitHub Issue][issues] 中提出你的想法。
- 如果你的想法被采纳,你可以在 GitHub 上[提交一个 Pull Request][new-pr]。
#### **你对源码有疑问?**
- 你可以在 [QQ 群][qq-group] 中与我们的开发者讨论,或者你也可以在 [GitHub Issue][issues] 中提出你的疑问。
#### **你想为文档做贡献?**
- 非常欢迎^^
- 你可以直接[提交一个 Pull Request][new-pr]。
炸毛框架是由志愿开发者共同维护的项目。我们欢迎你加入我们的开发。
感谢~ :heart: :heart: :heart:
炸毛框架开发团队
[issues]: https://github.com/zhamao-robot/zhamao-framework/issues
[new-pr]: https://github.com/zhamao-robot/zhamao-framework/pull/new/master
[new-issue]: https://github.com/zhamao-robot/zhamao-framework/issues/new
[qq-group]: https://jq.qq.com/?_wv=1027&k=KdIGy0UK

29
Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM ubuntu:18.04
WORKDIR /app/
RUN echo "Asia/Shanghai" > /etc/timezone
ENV LANG C.UTF_8
ENV LC_ALL C.UTF-8
ENV LANGUAGE C.UTF-8
RUN apt-get update && apt-get install -y software-properties-common tzdata
RUN dpkg-reconfigure -f noninteractive tzdata
VOLUME ["/app/zhamao-framework/"]
RUN add-apt-repository ppa:ondrej/php && \
apt-get update && \
apt-get install php php-dev php-mbstring gcc make openssl \
php-mbstring php-json php-curl php-mysql -y && \
apt-get install wget composer -y && \
wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \
tar -zxvf v4.5.0.tar.gz && \
cd swoole-src-4.5.0/ && \
phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \
(echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
ADD . /app/zhamao-framework
ADD . /app/zhamao-framework-bak
#RUN cd /app/zhamao-framework && composer update && composer clearcache
#RUN mv zhamao-framework-master zhamao-framework
WORKDIR /app/zhamao-framework
CMD ["/bin/bash", "-i", "/app/zhamao-framework-bak/.entry.sh"]

View File

@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 CrazyBot Developer Team
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

178
README.md
View File

@@ -1,146 +1,106 @@
<div align="center">
<img src="https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/logo_trans.png" width = "150" height = "150" alt="炸毛框架"><br>
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
<h2>炸毛框架</h2>
炸毛框架 (zhamao-framework) 是一个高性能的聊天机器人 + Web 服务器开发框架<br><br>
炸毛框架 (zhamao-frameowork) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
<p align="center">
<a href="https://onebot.dev/">
<img src="https://img.shields.io/badge/OneBot-12-black?style=flat-square" alt="OneBot">
</a>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![dev-version](https://img.shields.io/badge/dev--version-v2.0.0--a1-green)]()
<a href="https://github.com/zhamao-robot/zhamao-framework/actions">
<img src="https://img.shields.io/github/actions/workflow/status/zhamao-robot/zhamao-framework/test.yml?branch=v3-develop&label=Test&style=flat-square" alt="Integration Test">
</a>
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
<a href="https://packagist.org/packages/zhamao/framework">
<img src="https://img.shields.io/packagist/dt/zhamao/framework?label=Downloads&style=flat-square" alt="下载数">
</a>
</div>
<a href="https://github.com/zhamao-robot/zhamao-framework/releases">
<img src="https://img.shields.io/packagist/v/zhamao/framework?include_prereleases&label=Release&style=flat-square" alt="最新版本">
</a>
## 开发者注意
**此项目终于有开发讨论 QQ 群了群号670821194**
<a href="https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/zhamao-robot/zhamao-framework?label=License&style=flat-square" alt="开源协议">
</a>
**v2.0 版本已经开始公测了,但是文档还在光速编写中,可以现行进行测试!**
<a href="https://github.com/zhamao-robot/zhamao-framework/search?q=TODO">
<img src="https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO?label=TODO%20Counter&style=flat-square" alt="TODO">
</a>
</p>
**炸毛框架目前经过实验可以直接在 PHP8 环境上运行,但是细节部分未经充分测试,如果在 PHP8 环境下运行出现问题,请及时提出 Issue谢谢**
</div>
**由于 CQHTTP 不再提供维护,转为 [OneBot 标准](https://github.com/howmanybots/onebot)(原 CQHTTP 插件衍生而来的机器人 HTTP 接口标准),本框架也将在未来改为兼容此标准。**
开发者 QQ 群:**670821194** [点击加入群聊](https://jq.qq.com/?_wv=1027&k=YkNI3AIr)
**以上涉及的变更将在下一个大版本 (v2.0.0) 更新,请关注 2.0-dev 分支 和 Project 模块!**
**如果有愿意一起开发框架本身的开发者,请提出 PR 或 Issue 参与开发!如果对框架本身的核心设计有更好的想法,可与作者成立开发组(目前仅 2 人),参与 OneBot V12 生态和框架本身的开发。**
**相关正在进行的版本任务见 Projects 一栏!**
**v2.0版本即将到来,请持续关注 [新文档](https://docs-v2.zhamao.xin/) 进度和 Project 模块展示的测试进度!**
## 简介
zhamao-framework 是一个 PHP Swoole 的聊天机器人框架,兼容 OneBot 标准,它会对微信公众号等终端收到的消息进行解析处理,并以模块化的形式进行开发,来完成机器人的自然语言对话等功能。
炸毛框架使用 PHP 编写,主要面向 API 服务,聊天机器人,包含 WebsocketHTTP
等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的插件。
```php
#[\BotCommand('你好')]
public function hello(\BotContext $ctx) {
$ctx->reply("你好,我是炸毛!"); // 简单的命令式回复
}
#[\Route('/index')]
public function index() {
return "<h1>hello!</h1>"; // 快速的 HTTP 服务开发
}
```
除了起到解析消息的作用,炸毛框架 还提供了完整的 WebSocket + HTTP 服务器,你还能用此框架构建出高性能的 API 接口服务器。
## 开始
先安装环境,环境安装见下方文档。
1. `composer create-project zhamao/framework-starter` 从模板新建基础文档结构进行使用
2. 你也可以直接拉取本项目,进入文件夹后 `composer update` 加载依赖后使用 `bin/start init` 快速初始化框架文件
3. 还可以使用 Dockerfile 构建 Docker 容器
框架目前支持 Linux、WSL、macOS、Windows 环境直接运行,其中 Linux、macOS 环境可直接使用下方一键安装脚本。
## 文档 (v1.x)
国内服务器:[https://docs-v1.zhamao.xin/](https://docs-v1.zhamao.xin/)
> 如果你想在其他环境安装部署,可使用 Docker 快速部署或手动安装环境后安装框架,详见文档。
```bash
# Linux、macOS 下一键检测 PHP 环境、安装框架
bash <(curl -fsSL https://zhamao.xin/v3.sh)
# 启动框架
cd zhamao-v3
./zhamao server
```
一键安装脚本还有可以自定义参数的方式,比如:
```bash
# 脚本默认会检测系统的PHP如果想直接跳过检测安装独立的PHP版本则添加此环境变量
export ZM_NO_LOCAL_PHP="yes"
# 脚本如果安装独立版本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/v3.sh)
```
关于其他安装方式,请参阅 [文档](https://framework.zhamao.xin/guide/installation.html) 。
## 文档
查看文档(国内自建):<https://framework.zhamao.xin/>
备用链接(国外托管):<https://framework.zhamao.me/>
GitHub Pages[https://docs-v1.zhamao.me/](https://docs-v1.zhamao.me/)
## 特点
- 支持多账号
- 灵活的注解事件绑定机制
- 支持下断点调试Psysh
- 易用的上下文,模块内随处可用
- 采用模块化编写,功能之间高内聚低耦合
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL 查询器、数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
- 支持 phar 一键打包
- 原生支持多个机器人客户端同时连接
- 灵活的注解事件绑定机制,可同时使用 Annotation 和原生 Attribute 注解
- 完善的插件系统,可编写插件后打包或分发,供他人使用
- 采用插件化编写,可自由搭配其他 Composer 组件,也可单文件面向过程编写
- 支持模块打包、热加载,分享模块更方便
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
- 自带 MySQL、SQLite、Redis 等数据库连接池
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
- 可选自带 PHP 环境无需手动编译安装by [crazywhalecc/static-php-cli](https://github.com/crazywhalecc/static-php-cli)
## 炸毛特色模块
| 模块名称 | 说明 | 模块地址 |
| ------------------ | -------------------------------- | ------------------------------------------------------------ |
| 通用模块 | 图片上传和下载模块 | [zhamao-general-tools](https://github.com/zhamao-robot/zhamao-general-tools) |
## 计划开发内容
- [X] WebSocket测试脚本客户端
- [X] Session 和中间层管理模块
- [X] 常驻服务脚本
- [X] 一些常用的通用 API 例如经济(用户积分、亲密度等)的模块
- [ ] 图灵机器人/腾讯AI 聊天模块
- [ ] 分词模块(可能会放弃计划,因为目前好用的分词都是其他语言的)
- [ ] HTTP 过滤器、Auth 模块、完整的 MVC 兼容(可能会放弃计划,因为框架主打机器人开发)
- [ ] Redis 连接池或开箱即用的相应功能内置
- [X] 1.3 版本使用上下文代替
- [X] 更好的 Logger稳定和漂亮的控制台输出
- [ ] 日志服务
- [X] 框架支持 Phar 打包(可能会比较靠后支持)
- [ ] 完整的单元测试(如果有需求则尽快开发)
- [X] 静态文件服务器
## 从 cqbot-swoole 升级
目前新的框架采用了全新的注解机制,所以旧版的框架上写的模块到新框架需要重新编写。当然为了减少工作量,新的框架也最大限度地保留了旧版框架编写的风格,一般情况下根据新版框架的文档仅需修改少量地方即可完成重写。
旧版框架并入了 `old` 分支,如果想继续使用旧版框架请移步分支。升级过程中如果遇到问题可以找作者。
## 贡献和捐赠
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
目前项目仅两人维护,耗费精力较大,所以非常欢迎对框架的贡献。
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。目前项目仅一人维护耗费精力较大所以非常欢迎对框架的贡献。
本项目为作者闲暇时间开发,如果觉得好用,不妨进行捐助~你的捐助会让我更加有动力完善插件,感谢你的支持!
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
如果您不想直接参与框架的开发,也可以分享你编写的模块,帮助完善框架生态。
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
![支付宝二维码](https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
作者的炸毛机器人已从2018年初起稳定运行了**五年**,并且持续迭代
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 Issue 进行疑难解答
可以提交 [Issue](https://github.com/zhamao-robot/zhamao-framework/issues/new/choose) 或 [加群(670821194)](https://jq.qq.com/?_wv=1027&k=YkNI3AIr) 进行疑难解答
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份
项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的插件或项目代码做好备份
项目框架采用 Apache-2.0 协议开源,在分发或重写修改等操作时需遵守协议。项目插件部分(除 `src/Globals``src/ZM` 文件夹外的其他文件夹) 在非借鉴框架内代码时可不遵守 Apache-2.0 协议进行分发和修改(声明版权)。
项目框架采用 Apache-2.0 协议开源,在分发或重写修改等操作时需遵守协议。项目模块部分(`Module` 文件夹) 在非借鉴框架内代码时可不遵守 Apache-2.0 协议进行分发和修改(声明版权)
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
感谢 JetBrains 为此开源项目提供 PhpStorm 开发工具支持:
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/PhpStorm.svg" width="300">
感谢开发者 @sunxyw 中为项目开发规范化提出的一些建议。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.0 | :white_check_mark: |
| 1.6.x | :white_check_mark: |
| 1.x | :x: |
## Reporting a Vulnerability
If you find a bug which is safety related, you should post a new issue named **Security Issue**, and I will check it as soon as possible.

60
bin/phar-build Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env php
<?php /** @since 1.2.1 */
global $version;
echo "version: " . ($version = json_decode(file_get_contents(__DIR__ . "/../composer.json"), true)["version"]) . PHP_EOL;
switch ($argv[1] ?? '') {
case '--normal':
case '':
build();
break;
case '--help':
case '-h':
default:
echo "\nzhamao-framework Phar builder.\n";
echo "\nUsage: " . $argv[0] . " [OPTION]";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n --with-wechat-patch\tReplace ModBase with wechat patch version and build your own phar package";
echo "\n --normal\t\tBuild your own phar package as normal options\n\n";
break;
}
function build($with_wechat_patch = false) {
if (ini_get('phar.readonly') == 1) {
die("You need to set \"phar.readonly\" to \"Off\"!\nSee: https://stackoverflow.com/questions/34667606/cant-enable-phar-writing\n");
}
$filename = "server.phar";
@unlink(__DIR__ . '/../resources/' . $filename);
$phar = new Phar(__DIR__ . '/../resources/' . $filename);
$phar->startBuffering();
$src = realpath(__DIR__ . '/../');
$hello = file_get_contents($src . '/src/Module/Example/Hello.php');
$middleware = file_get_contents($src . '/src/Module/Middleware/TimerMiddleware.php');
unlink($src . '/src/Module/Example/Hello.php');
unlink($src . '/src/Module/Middleware/TimerMiddleware.php');
if ($with_wechat_patch) {
global $wechat_patch;
$wechat = base64_decode($wechat_patch);
} else {
$wechat = false;
}
if ($wechat !== false) {
echo "Using wechat patch.\n";
$modbase = file_get_contents($src . '/src/ZM/ModBase.php');
unlink($src . '/src/ZM/ModBase.php');
}
$phar->buildFromDirectory($src);
$phar->addFromString('tmp/Hello.php.bak', $hello);
$phar->addFromString('tmp/TimerMiddleware.php.bak', $middleware);
if ($wechat !== false) {
$phar->addFromString('src/ZM/ModBase.php', $wechat);
file_put_contents($src . '/src/ZM/ModBase.php', $modbase);
}
//$phar->compressFiles(Phar::GZ);
$phar->setStub($phar->createDefaultStub('phar-starter.php'));
$phar->stopBuffering();
file_put_contents($src . '/src/Module/Example/Hello.php', $hello);
file_put_contents($src . '/src/Module/Middleware/TimerMiddleware.php', $middleware);
echo "Successfully built. Location: " . $src . "/resources/$filename\n";
}

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env php
<?php
/** For Swoole coroutine tests */
// 如果改成 true则会在终端显示所有炸毛框架的 Log
const ZM_TEST_LOG_DEBUG = false;
use OneBot\Driver\Event\Process\WorkerStartEvent;
use PHPUnit\TextUI\Command;
use Swoole\Atomic;
use Tests\ZMResultPrinter;
use ZM\Command\Server\ServerStartCommand;
use ZM\Event\EventProvider;
use ZM\Framework;
use ZM\Logger\ConsoleLogger;
use ZM\Store\MockAtomic;
// 引入自动加载
require $_composer_autoload_path ?? dirname(__DIR__) . '/vendor/autoload.php';
// 声明一个全局的原子计数,用于保存整个进程的退出状态码
global $_swoole_atomic;
// Windows 没有 Swoole所以就使用了一个假的原子计数器反正 Windows 只有单进程
$_swoole_atomic = class_exists('\\Swoole\\Atomic') ? new Atomic() : new MockAtomic();
// 注册 Logger 等级
ob_logger_register(new ConsoleLogger(ZM_TEST_LOG_DEBUG ? 'debug' : 'error'));
// 使用框架自身的 EventProvider
global $ob_event_provider;
$ob_event_provider = new EventProvider();
// 注册一个最低级别的 WorkerStart 事件,用于在框架的事件初始化之后开始运行 PHPUnit
ob_event_provider()->addEventListener(WorkerStartEvent::getName(), function () {
register_shutdown_function(function () {
$error = error_get_last();
// 下面这段代码的作用就是,不是错误引发的退出时照常退出即可
if (($error['type'] ?? 0) != 0) {
logger()->emergency(zm_internal_errcode('E00027') . 'Internal fatal error: ' . $error['message'] . ' at ' . $error['file'] . "({$error['line']})");
}
Framework::getInstance()->stop();
});
$_SERVER['argv'][] = '--printer';
$_SERVER['argv'][] = ZMResultPrinter::class;
try {
// 不退出,而是返回 code
$retcode = Command::main(false);
} finally {
global $_swoole_atomic;
$_swoole_atomic->set($retcode ?? 0);
Framework::getInstance()->stop($retcode ?? 0);
}
}, 1);
try {
$options = ServerStartCommand::exportOptionArray();
$options['driver'] = 'workerman';
$options['worker-num'] = 1;
$options['private-mode'] = true;
(new Framework($options))->init()->start();
exit($_swoole_atomic->get());
} catch (Throwable $e) {
echo $e->getMessage() . PHP_EOL;
exit(1);
}

View File

@@ -1,3 +0,0 @@
@echo off
php %~dp0phpunit-zm %*

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env php
<?php
function find_valid_root(string $current_dir, int $max_loop): string
{
if ($max_loop <= 0) {
return '';
}
if (!file_exists($current_dir . '/composer.json')) {
return find_valid_root(dirname($current_dir), $max_loop - 1);
}
return $current_dir;
}
$root = find_valid_root(getcwd(), 3);
if (empty($root)) {
echo '找不到有效的根目录';
exit(1);
}
chdir($root);
require_once $root . '/vendor/autoload.php';
// 获取参数,可为 before 或 after
$param = $argv[1] ?? 'before';
if ($param === 'before') {
// Doxyfile file source: https://pastebin.com/raw/N8tJ9kWE
// Posted by: sunxyw
// 从剪贴板获取 Doxyfile 内容并保存到临时文件
logger()->info('正在获取 Doxyfile 内容');
$doxyfile = file_get_contents('https://pastebin.com/raw/N8tJ9kWE');
$doxyfile = str_replace('<zm_version>', \ZM\Framework::VERSION, $doxyfile);
file_put_contents('Doxyfile', $doxyfile);
// 应用 Awesome 样式
// 来源https://github.com/jothepro/doxygen-awesome-css.git
// 优先使用本地文件
logger()->info('正在应用 Awesome 样式');
\ZM\Store\FileSystem::createDir('doxy/css');
if (
file_exists('doxy/css/doxygen-awesome.css') &&
(md5_file('doxy/css/doxygen-awesome.css') === '326a1447f9693d1b3876f59de837a7c0')
) {
logger()->info('本地 Awesome 样式文件已存在,跳过下载');
} else {
logger()->info('正在下载 Awesome 样式文件');
$cmd = [
'git clone https://github.com/jothepro/doxygen-awesome-css.git doxy/css',
'cd doxy/css',
'git checkout v2.1.0',
'cd ../..',
];
$cmd = implode(' && ', $cmd);
exec($cmd);
}
} elseif ($param === 'after') {
// 删除临时文件
unlink('Doxyfile');
// 清除旧文档
exec('rm -rf docs/.vuepress/public/doxy');
// 授予目录权限doxy/html
exec('sudo chmod -R 644 doxy/html');
// 移动新文档到 docs 目录
exec('mv doxy/html docs/.vuepress/public/doxy');
logger()->info('文档生成完成');
} else {
logger()->error('参数错误');
exit(1);
}

135
bin/start Executable file
View File

@@ -0,0 +1,135 @@
#!/usr/bin/env php
<?php
use Framework\FrameworkLoader;
use Scheduler\Scheduler;
require __DIR__ . '/../src/Framework/FrameworkLoader.php';
require __DIR__ . '/../src/Scheduler/Scheduler.php';
Swoole\Coroutine::set([
'max_coroutine' => 30000,
]);
global $vendor_mode;
$vendor_mode = false;
if (mb_strpos(__DIR__, getcwd()) !== false && substr(str_replace(getcwd(), "", __DIR__), 0, 8) == "/vendor/") {
define("LOAD_MODE", 1); //composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
} else {
define("LOAD_MODE", 0); //正常模式
}
date_default_timezone_set("Asia/Shanghai");
switch ($argv[1] ?? '') {
case 'scheduler':
case 'timer':
go(function () {
try {
new Scheduler(Scheduler::REMOTE);
} catch (Exception $e) {
die($e->getMessage());
}
});
break;
case 'phar-build':
array_shift($argv);
require_once 'phar-build';
break;
case 'systemd':
array_shift($argv);
require_once 'systemd';
break;
case 'init':
array_shift($argv);
if (LOAD_MODE != 1) {
echo "initialization must be started with composer-project mode!\n";
exit(1);
}
$cwd = LOAD_MODE_COMPOSER_PATH;
echo "Copying default module file ...";
@mkdir($cwd . "/config");
@mkdir($cwd . "/src");
@mkdir($cwd . "/src/Custom");
@mkdir($cwd . "/src/Module");
@mkdir($cwd . "/src/Module/Example");
@mkdir($cwd . "/src/Module/Middleware");
$ls = [
"/config/global.php",
"/.gitignore",
"/config/file_header.json",
"/config/motd.txt",
"/src/Module/Example/Hello.php",
"/src/Module/Middleware/TimerMiddleware.php",
"/src/Custom/global_function.php"
];
foreach($ls as $v) {
if(!file_exists($cwd.$v)) {
echo "Copying ".$v.PHP_EOL;
copy($cwd."/vendor/zhamao/framework".$v, $cwd.$v);
}
}
$autoload = [
"psr-4" => [
"Module\\" => "src/Module",
"Custom\\" => "src/Custom"
],
"files" => [
"src/Custom/global_function.php"
]
];
$scripts = [
"server" => "vendor/bin/start server",
"server:log-debug" => "vendor/bin/start server --log-debug",
"server:log-verbose" => "vendor/bin/start server --log-verbose",
"server:log-info" => "vendor/bin/start server --log-info",
"server:log-warning" => "vendor/bin/start server --log-warning",
"server:debug-mode" => "vendor/bin/start server --debug-mode",
"systemd" => "vendor/bin/start systemd"
];
echo PHP_EOL;
if (file_exists($cwd . "/composer.json")) {
echo "Updating composer.json ...";
$composer = json_decode(file_get_contents($cwd . "/composer.json"), true);
if (!isset($composer["autoload"])) {
$composer["autoload"] = $autoload;
}
if (!isset($composer["scripts"])) {
$composer["scripts"] = $scripts;
}
file_put_contents($cwd . "/composer.json", json_encode($composer, 64 | 128 | 256));
echo PHP_EOL;
} else {
echo("Error occurred. Please check your updates.\n");
exit(1);
}
echo "success!\n";
break;
case '':
case 'framework':
case 'server':
if (!is_dir(__DIR__ . '/../vendor/') && LOAD_MODE == 0) {
echo "Warning: you have not update composer!\n";
exec("composer update", $out, $var);
if ($var != 0) {
echo "You need to run \"composer update\" at root of zhamao-framework!\n";
die;
}
}
$loader = new FrameworkLoader($argv);
break;
case '--help':
case '-h':
echo "\nUsage: " . $argv[0] . " [OPTION]\n";
echo "\nzhamao-framework start script, provides several startup arguments.";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n framework, server\tstart main framework, this is default option";
echo "\n phar-build\t\tbuild a new phar archive";
echo "\n init\t\t\tinitialize framework structure in this directory";
echo "\n systemd\t\tgenerate a new systemd \".service\" file to use\n\n";
break;
default:
echo "Unknown option \"{$argv[1]}\"!\n\"--help\" for more information\n";
break;
}

31
bin/systemd Normal file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env php
<?php /** @since 1.2 */
switch ($argv[1] ?? '') {
case '--generate':
case '':
generate($argv);
break;
case '--help':
case '-h':
default:
echo "\nUsage: " . $argv[0] . " [OPTION]\n";
echo "\nzhamao-framework systemd generator.";
echo "\n\n -h, --help\t\tShow this help menu";
echo "\n --generate\tGenerate a systemd service file\n\n";
break;
}
function generate($argv) {
$s = "[Unit]\nDescription=zhamao-framework Daemon\nAfter=rc-local.service\n\n[Service]\nType=simple";
$s .= "\nUser=" . exec("whoami");
$s .= "\nGroup=" . exec("groups | awk '{print $1}'");
$s .= "\nWorkingDirectory=" . getcwd();
if ($argv[0] == "systemd" && !file_exists(getcwd() . '/systemd'))
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server --disable-console-input";
else
$s .= "\nExecStart=" . getcwd() . "/bin/start server --disable-console-input";
$s .= "\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n";
@mkdir(getcwd() . "/resources/");
file_put_contents(getcwd() . "/resources/zhamao.service", $s);
echo "File successfully generated. Path: " . getcwd() . "/resources/zhamao.service\n";
}

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env bash
_red="\e[33m"
_green="\e[32m"
_reset="\e[0m"
unix_s=$(uname -s)
unix_release=$(
marked_release=""
if [ "$unix_s" = "Linux" ]; then
echo $HOME | grep com.termux >/dev/null
if [ $? == 0 ]; then
marked_release="termux"
elif [ -f "/etc/redhat-release" ]; then
if [ "$(cat /etc/redhat-release | awk '{print $1}' | grep -v '^$')" = "CentOS" ]; then
marked_release="CentOS"
else
marked_release="unknown"
fi
elif [ -f "/etc/os-release" ]; then
cat /etc/os-release | grep Alpine > /dev/null
if [ $? == 0 ]; then
marked_release="Alpine"
fi
fi
if [ "$marked_release" = "" ]; then
if [ -f "/etc/issue" ]; then
marked_release=$(cat /etc/issue | grep -v '^$' | awk '{print $1}')
else
marked_release="unknown"
fi
fi
elif [ "$unix_s" = "Darwin" ]; then
marked_release=$(sw_vers | grep ProductName | awk '{print $2" "$3" "$4}')
fi
echo $marked_release
)
unix_release=$(echo $unix_release | xargs)
function echo_error() {
echo -e "${_red}$1${_reset}"
}
function echo_info() {
echo -e "${_green}$1${_reset}"
}
function install_test() {
which fswatch >/dev/null
if [ $? != 0 ]; then
operate_confirm "fswatch还没有安装是否确认安装" && install_fswatch
fi
}
function install_fswatch() {
if [ "$unix_s" = "Linux" ]; then
case $unix_release in
"Kali" | "Ubuntu" | "Debian" | "Raspbian" | 'Pop!_OS')
sudo apt-get install fswatch -y ;;
#"termux") pkg install $1 -y ;;
"CentOS")
curl -o fswatch.tgz https://download.fastgit.org/emcrisostomo/fswatch/releases/download/1.16.0/fswatch-1.16.0.tar.gz && \
tar -xzf fswatch.tgz && \
cd fswatch-1.16.0 && \
./configure && \
make && \
sudo make install && \
cd .. && \
rm -rf fswatch.tgz fswatch-1.16.0
;;
#"Alpine") apk add $1 ;;
*)
echo_error "不支持的 Linux 发行版:$unix_release"
exit 1
;;
esac
elif [ "$unix_s" = "Darwin" ]; then
brew install fswatch
else
echo_error "不支持的操作系统:$unix_s"
exit 1
fi
}
function operate_confirm() {
echo -n $(echo_info "$1 [Y/n] ")
read operate
operate=$(echo $operate | tr A-Z a-z)
if [[ "$operate" = "y" || "$operate" = "" ]]; then
return 0
else
return 1
fi
}
echo_info "当前系统:$unix_release"
install_test
if [ $? -ne 0 ]; then
exit 1
else
echo_info "程序路径:$(which fswatch)"
fi
watch_dir="./src"
if [ ! -d "$watch_dir" ]; then
echo_error "src目录不存在"
exit 1
else
echo_info "监听目录:$watch_dir"
fi
_pid=$(cat .daemon_pid | awk -F"\"pid\": " '{print $2}' | grep -v ^$ | sed 's/,//g')
if [ "$_pid" = "" ]; then
echo_error "未检测到框架进程"
exit 1
fi
fswatch $watch_dir | while read file
do
echo "Detect file change: $file"
kill -USR1 $_pid
done

View File

@@ -1,31 +0,0 @@
#!/bin/sh
# shellcheck disable=SC2068
# shellcheck disable=SC2181
# author: crazywhalecc
# since: 2.5.0
if [ "$ZM_CUSTOM_PHP_PATH" != "" ]; then
echo "* Using PHP executable: ""$ZM_CUSTOM_PHP_PATH"
executable="$ZM_CUSTOM_PHP_PATH"
elif [ -f "$(pwd)/runtime/php" ]; then
executable="$(pwd)/runtime/php"
echo "* Framework started with built-in php."
else
which php >/dev/null 2>&1
if [ $? -eq 0 ]; then
executable=$(which php)
else
echo '[ErrCode:E00014] Cannot find any PHP runtime, please use command "./install-runtime.sh" or install PHP manually!'
exit 1
fi
fi
if [ -f "$(pwd)/src/entry.php" ]; then
$executable "$(pwd)/src/entry.php" $@
elif [ -f "$(pwd)/vendor/zhamao/framework/src/entry.php" ]; then
$executable "$(pwd)/vendor/zhamao/framework/src/entry.php" $@
else
echo "[ErrCode:E00015] Cannot find zhamao-framework entry file!"
exit 1
fi

View File

@@ -1,27 +0,0 @@
@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
)
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
)

View File

@@ -1,47 +0,0 @@
{
"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

@@ -1,103 +1,37 @@
{
"name": "zhamao/framework",
"description": "High performance chat robot and web server development framework",
"description": "High performance QQ robot and web server development framework",
"minimum-stability": "stable",
"license": "Apache-2.0",
"version": "1.6.5",
"authors": [
{
"name": "jerry",
"email": "admin@zhamao.me"
"name": "whale",
"email": "crazysnowcc@gmail.com"
},
{
"name": "sunxyw",
"email": "dev@sunxyw.xyz"
"name": "swift",
"email": "hugo_swift@yahoo.com"
}
],
"prefer-stable": true,
"bin": [
"bin/start"
],
"require": {
"php": "^8.0 || ^8.1 || ^8.2",
"php": ">=7.2",
"swoole/ide-helper": "@dev",
"swlib/saber": "^1.0",
"doctrine/annotations": "~1.10",
"ext-json": "*",
"ext-tokenizer": "*",
"doctrine/dbal": "^2.13.1",
"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",
"symfony/polyfill-mbstring": "^1.19",
"symfony/routing": "~6.0 || ~5.0 || ~4.0"
"psy/psysh": "@stable",
"symfony/polyfill-ctype": "^1.20",
"symfony/polyfill-mbstring": "^1.20"
},
"require-dev": {
"captainhook/captainhook": "^5.12",
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",
"jangregor/phpstan-prophecy": "^1.0",
"jetbrains/phpstorm-attributes": "^1.0",
"mikey179/vfsstream": "^1.6",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0",
"roave/security-advisories": "dev-latest",
"swoole/ide-helper": "^4.5"
},
"replace": {
"symfony/polyfill-php80": "*"
},
"suggest": {
"ext-ctype": "Use C/C++ extension instead of polyfill will be more efficient",
"ext-mbstring": "Use C/C++ extension instead of polyfill will be more efficient",
"ext-pdo_mysql": "If you use mysql in framework, you will need this extension",
"ext-redis": "If you use Redis in framework, you will need this extension",
"league/climate": "Display columns and status in terminal"
},
"minimum-stability": "dev",
"prefer-stable": true,
"autoload": {
"psr-4": {
"Framework\\": "src/Framework",
"ZM\\": "src/ZM"
},
"files": [
"src/Globals/global_functions.php",
"src/Globals/global_defines_app.php",
"src/Globals/global_class_alias.php"
]
},
"autoload-dev": {
"psr-4": {
"Module\\": "src/Module",
"Tests\\": "tests"
}
},
"bin": [
"bin/phpunit-zm",
"bin/zhamao",
"bin/zhamao.bat"
],
"config": {
"allow-plugins": {
"phpstan/extension-installer": true
},
"optimize-autoloader": true,
"sort-packages": true
},
"extra": {
"zm": {
"exclude-annotation-path": [
"src/ZM",
"tests"
]
}
},
"scripts": {
"post-autoload-dump": "vendor/bin/captainhook install -f -s",
"analyse": "phpstan analyse --memory-limit 300M",
"cs-fix": "PHP_CS_FIXER_FUTURE_MODE=1 php-cs-fixer fix",
"test": "bin/phpunit-zm --no-coverage"
}
}
}

View File

@@ -1,28 +0,0 @@
<?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,29 +0,0 @@
{
"default": {
"success": "green",
"info": "lightblue",
"warning": "yellow",
"error": "red",
"verbose": "blue",
"debug": "gray",
"trace": "gray"
},
"white-term": {
"success": "green",
"info": "",
"warning": "yellow",
"error": "red",
"verbose": "blue",
"debug": "gray",
"trace": "gray"
},
"no-color": {
"success": "",
"info": "",
"warning": "",
"error": "",
"verbose": "",
"debug": "",
"trace": ""
}
}

View File

@@ -1,28 +0,0 @@
<?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/
* 我们建议你在使用容器前先阅读以下章节:
* - 基本使用方式https://php-di.org/doc/container.html
* - 注册绑定方式https://php-di.org/doc/php-definitions.html#definition-types
* - 最佳使用实践https://php-di.org/doc/best-practices.html
* 同时,你也可以选择查阅我们(炸毛框架)的相关文档,其会包含一些简单的使用方法和示例。
* 关于框架在不同事件(或周期中)默认注册的绑定,请参阅我们的文档,或直接查看 {@see \ZM\Container\ContainerRegistrant}
*/
return [
// 这里定义的是全局容器的绑定,不建议在此处直接调用框架、应用内部的类或方法,因为这些类可能还没有被加载或初始化
// 你可以使用匿名函数来延迟加载
'definitions' => [
'worker_id' => fn () => ProcessManager::getProcessId(),
Driver::class => fn () => Framework::getInstance()->getDriver(),
LoggerInterface::class => fn () => logger(),
],
];

View File

@@ -1,125 +1,94 @@
<?php
global $config;
declare(strict_types=1);
/** bind host */
$config['host'] = '0.0.0.0';
/* 启动框架的底层驱动(原生支持 swoole、workerman 两种) */
$config['driver'] = 'workerman';
/** bind port */
$config['port'] = 20001;
/* 要启动的服务器监听端口及协议 */
$config['servers'] = [
[
'host' => '0.0.0.0',
'port' => 20001,
'type' => 'websocket',
],
[
'host' => '0.0.0.0',
'port' => 20002,
'type' => 'http',
'flag' => 20002,
],
/** 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = "http://127.0.0.1:" . $config['port'];
/** 框架是否启动debug模式 */
$config['debug_mode'] = false;
/** 存放框架内文件数据的目录 */
$config['zm_data'] = realpath(__DIR__ . "/../") . '/zm_data/';
/** 存放各个模块配置文件的目录 */
$config['config_dir'] = $config['zm_data'] . 'config/';
/** 存放崩溃和运行日志的目录 */
$config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'worker_num' => 1,
'dispatch_mode' => 2,
//'task_worker_num' => 1,
//'task_enable_coroutine' => true
];
/* Workerman 驱动相关配置 */
$config['workerman_options'] = [
'worker_num' => 1, // 如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
];
/* Swoole 驱动相关配置 */
$config['swoole_options'] = [
'coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL), // 协程 Hook 内容
'swoole_set' => [
'worker_num' => 1, // 如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
'dispatch_mode' => 2, // 包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'max_coroutine' => 300000, // 允许最大的协程数
'max_wait_time' => 5, // 安全退出模式下允许等待 Worker 的最长秒数
// 'task_worker_num' => 4, // 启动 TaskWorker 进程的数量(默认不启动)
// 'task_enable_coroutine' => true // TaskWorker 是否开启协程
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['sql_config'] = [
'sql_host' => '',
'sql_port' => 3306,
'sql_username' => 'name',
'sql_database' => 'db_name',
'sql_password' => '',
'sql_enable_cache' => true,
'sql_reset_cache' => '0300',
'sql_options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false
],
'swoole_server_mode' => SWOOLE_PROCESS, // Swoole Server 启动模式,默认为 SWOOLE_PROCESS
'sql_no_exception' => false,
'sql_default_fetch_mode' => PDO::FETCH_BOTH // added in 1.5.6
];
/* 默认存取炸毛数据的目录相对目录时代表WORKING_DIR下的目录绝对目录按照绝对目录来 */
$config['data_dir'] = WORKING_DIR . '/zm_data';
/** CQHTTP连接约定的token */
$config["access_token"] = "";
/* 框架本体运行时的一些可调配置 */
$config['runtime'] = [
'reload_delay_time' => 800,
'annotation_reader_ignore' => [ // 设置注解解析器忽略的注解名或命名空间,防止解析到不该解析的
'name' => [
'mixin',
],
'namespace' => [],
],
'timezone' => 'Asia/Shanghai',
/** HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'X-Powered-By' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
];
/* 上下文接口类 implemented from ContextInterface */
/** HTTP服务器在指定状态码下回复的页面默认 */
$config['http_default_code_page'] = [
'404' => '404.html'
];
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
'in_count' => 0, //消息接收message的统计数量
'out_count' => 0, //消息发送调用send_*_msg的统计数量
'reload_time' => 0, //调用reload功能统计数量
'wait_msg_id' => 0, //协程挂起id自增
'info_level' => 2, //终端显示的log等级
];
/** 自动保存的缓存保存时间(秒) */
$config['auto_save_interval'] = 900;
/** 上下文接口类 implemented from ContextInterface */
$config['context_class'] = \ZM\Context\Context::class;
/* 允许加载插件形式 */
$config['plugin'] = [
'enable' => true,
'load_dir' => 'plugins',
/** 静态文件访问 */
$config['static_file_server'] = [
'status' => false,
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
'document_index' => [
'index.html'
]
];
/* 内部默认启用的插件 */
$config['native_plugin'] = [
'onebot12' => true, // OneBot v12 协议支持
'onebot12-ban-other-ws' => true, // OneBot v12 协议支持,禁止其他 WebSocket 连接
'command-manual' => true,
];
/* 静态文件读取器 */
$config['file_server'] = [
'enable' => true,
'document_root' => $config['data_dir'] . '/public/',
'document_index' => 'index.html',
'document_code_page' => [
'404' => '404.html',
'500' => '500.html',
],
];
/* MySQL 和 SQLite3 数据库连接配置,框架将自动生成连接池,支持多个连接池 */
$config['database'] = [
'sqlite_db1' => [
'enable' => false,
'type' => 'sqlite',
'dbname' => 'a.db',
'pool_size' => 10,
],
'default' => [
'enable' => false,
'type' => 'mysql',
'host' => '127.0.0.1', // 填写数据库服务器地址后才会创建数据库连接
'port' => 3306,
'username' => 'root',
'password' => 'ZhamaoTEST',
'dbname' => 'zm',
'charset' => 'utf8mb4',
'pool_size' => 64,
],
];
/* Redis 连接配置,框架将自动生成连接池,支持多个连接池 */
$config['redis'] = [
'default' => [
'enable' => false,
'host' => '127.0.0.1',
'port' => 6379,
'index' => 0,
'auth' => '',
'pool_size' => 10,
],
];
/* KV 数据库的配置 */
$config['kv'] = [
'use' => \LightCache::class, // 默认在单进程模式下使用 LightCache多进程需要使用 ZMRedis
'light_cache_dir' => $config['data_dir'] . '/lc', // 默认的 LightCache 保存持久化数据的位置
'light_cache_autosave_time' => 600, // LightCache 自动保存时间(秒)
'redis_config' => 'default',
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\Framework\ServerEventHandler::class,
];
return $config;

View File

@@ -1,152 +0,0 @@
<?php
/** @noinspection PhpComposerExtensionStubsInspection */
declare(strict_types=1);
/* bind host */
$config['host'] = '0.0.0.0';
/* bind port */
$config['port'] = 20001;
/* 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = 'http://127.0.0.1:' . $config['port'];
/* 框架是否启动debug模式当debug模式为true时启用热更新需要安装inotify扩展 */
$config['debug_mode'] = false;
/* 存放框架内文件数据的目录 */
$config['zm_data'] = realpath(WORKING_DIR) . '/zm_data/';
/* 存放各个模块配置文件的目录 */
$config['config_dir'] = $config['zm_data'] . 'config/';
/* 存放崩溃和运行日志的目录 */
$config['crash_dir'] = $config['zm_data'] . 'crash/';
/* 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
// 'worker_num' => swoole_cpu_num(), //如果你只有一个 OneBot 实例连接到框架并且代码没有复杂的CPU密集计算则可把这里改为1使用全局变量
'dispatch_mode' => 2, // 包分配原则,见 https://wiki.swoole.com/#/server/setting?id=dispatch_mode
'max_coroutine' => 300000,
'max_wait_time' => 5,
// 'task_worker_num' => 4,
// 'task_enable_coroutine' => true
];
/* 一些框架与框架运行时设置的调整 */
$config['runtime'] = [
'swoole_coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL),
'swoole_server_mode' => SWOOLE_PROCESS,
'middleware_error_policy' => 1,
'reload_delay_time' => 800,
'global_middleware_binding' => [],
'save_console_log_file' => false, // 改为目标路径,则将 Console 输出的日志保存到文件
'annotation_reader_ignore' => [ // 设置注解解析器忽略的注解名或命名空间,防止解析到不该解析的
'name' => [
'mixin',
],
'namespace' => [],
],
];
/* 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
'size' => 512, // 最多允许储存的条数需要2的倍数
'max_strlen' => 32768, // 单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, // Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'] . '_cache.json',
'auto_save_interval' => 900,
];
/* 大容量跨进程变量存储2.2.0可用) */
$config['worker_cache'] = [
'worker' => 0,
'transaction_timeout' => 30000,
];
/* MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['mysql_config'] = [
'host' => '',
'port' => 3306,
'unix_socket' => null,
'username' => 'root',
'password' => '123456',
'dbname' => '',
'charset' => 'utf8mb4',
'pool_size' => 64,
'options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
],
];
/* Redis连接信息host留空则启动时不创建Redis连接池 */
$config['redis_config'] = [
'host' => '',
'port' => 6379,
'timeout' => 1,
'db_index' => 0,
'auth' => '',
];
/* onebot连接约定的token */
$config['access_token'] = '';
/* HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'Server' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8',
];
/* HTTP服务器在指定状态码下回复的页面默认 */
$config['http_default_code_page'] = [
'404' => '404.html',
];
/* zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
// 'custom_atomic_name' => 0, //自定义添加的Atomic
];
/* 终端日志显示等级0-4 */
$config['info_level'] = 2;
/* 上下文接口类 implemented from ContextInterface */
$config['context_class'] = \ZM\Context\Context::class;
/* 静态文件访问 */
$config['static_file_server'] = [
'status' => false,
'document_root' => realpath(__DIR__ . '/../') . '/resources/html',
'document_index' => [
'index.html',
],
];
/* 机器人解析模块关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
$config['onebot'] = [
'status' => true,
'single_bot_mode' => false,
'message_level' => 99,
'message_convert_string' => true,
'message_command_policy' => 'interrupt',
];
/* 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */
$config['remote_terminal'] = [
'status' => false,
'host' => '127.0.0.1',
'port' => 20002,
'token' => '',
];
/* 模块(插件)加载器的相关设置 */
$config['module_loader'] = [
'enable_hotload' => false,
'load_path' => $config['zm_data'] . 'modules',
];
return $config;

View File

@@ -1,6 +1,6 @@
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/

View File

@@ -1,96 +0,0 @@
version: "3.9"
services:
bot:
build:
context: docker/zhamao
dockerfile: ./Dockerfile
container_name: zhamao-bot
depends_on:
- database-postgres
- database-redis
networks:
- bot-net
ports:
- "20001-20005:20001-20005/tcp"
volumes:
- ".:/bot"
walle-q:
build:
context: docker/walle-q
dockerfile: ./Dockerfile
container_name: zhamao-bot-walle-q
depends_on:
- bot
networks:
- bot-net
volumes:
- "./docker/volumes/walle-q-data:/bot"
database-postgres:
container_name: zhamao-bot-db-postgres
env_file:
- docker/environment.env
image: postgres:14.4-alpine
networks:
- bot-net
ports:
- "5432:5432/tcp"
restart: always
volumes:
- "./docker/volumes/postgres-data:/var/lib/postgresql/data"
- "./docker/volumes/postgres-init:/docker-entrypoint-initdb.d"
database-redis:
command:
- redis-server
- --requirepass
- "$${REDIS_PASSWORD}"
container_name: zhamao-bot-db-redis
env_file:
- docker/environment.env
image: redis:7.0-alpine
networks:
- bot-net
ports:
- "6379:6379/tcp"
restart: always
volumes:
- "./docker/volumes/redis-data:/data"
db-admin-adminer:
container_name: zhamao-bot-dbadmin-adminer
depends_on:
database-postgres:
condition: service_started
database-redis:
condition: service_started
env_file:
- docker/environment.env
image: adminer:latest
networks:
- bot-net
ports:
- "15432:8080/tcp"
profiles:
- with-adminer
db-admin-redis-insight:
container_name: zhamao-bot-dbadmin-redis-insight
depends_on:
database-postgres:
condition: service_started
database-redis:
condition: service_started
image: redislabs/redisinsight:latest
networks:
- bot-net
ports:
- "16379:8001/tcp"
profiles:
- with-adminer
networks:
bot-net:
name: bot-net

2
docker/.gitignore vendored
View File

@@ -1,2 +0,0 @@
/volumes
environment.env

View File

@@ -1,7 +0,0 @@
REDIS_PASSWORD={{$REDIS_PASSWORD}}
POSTGRES_USER={{$DB_ROOT_USER}}
POSTGRES_PASSWORD={{$DB_ROOT_PASSWORD}}
POSTGRES_APPLICATION_USER={{$DB_USER}}
POSTGRES_APPLICATION_USER_PASSWORD={{$DB_PASSWORD}}
POSTGRES_APPLICATION_DATABASE={{$DB_NAME}}

View File

@@ -1,14 +0,0 @@
FROM alpine:latest
MAINTAINER sunxyw <me@sunxyw.xyz>
RUN apk add --no-cache curl
RUN mkdir -p /bot && \
curl -fsSL https://github.com/onebot-walle/walle-q/releases/latest/download/walle-q-i686-linux-musl.tar.gz -O && \
tar -zxvf walle-q-i686-linux-musl.tar.gz -C /bot && \
rm -rf walle-q-i686-linux-musl.tar.gz && \
chmod +x /bot/walle-q-i686-linux-musl
ENV TZ=Asia/Shanghai
ENTRYPOINT [ "/bot/walle-q-i686-linux-musl" ]

View File

@@ -1,14 +0,0 @@
FROM phpswoole/swoole:5.0-php8.1
MAINTAINER sunxyw <me@sunxyw.xyz>
WORKDIR "/bot"
ENV TZ=Asia/Shanghai
EXPOSE 20001-20005
RUN docker-php-ext-install pcntl
ENTRYPOINT [ "./zhamao" ]
CMD [ "server" ]

18
docker_mixed/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM richardchien/cqhttp:latest
RUN apt-get update && apt-get install -y software-properties-common tzdata
RUN dpkg-reconfigure -f noninteractive tzdata
RUN add-apt-repository ppa:ondrej/php && \
apt-get update && \
apt-get install php php-dev php-mbstring gcc make openssl \
php-mbstring php-json php-curl php-mysql -y && \
apt-get install wget composer -y && \
wget https://github.com/swoole/swoole-src/archive/v4.5.0.tar.gz && \
tar -zxvf v4.5.0.tar.gz && \
cd swoole-src-4.5.0/ && \
phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && \
(echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
ADD start.sh /home/user/start.sh
RUN chown user:user /home/user/start.sh && chmod +x /home/user/start.sh
ADD https://github.com/zhamao-robot/zhamao-framework/archive/master.zip /home/user/master.zip
RUN chown user:user /home/user/master.zip && chmod 777 /home/user/master.zip
VOLUME ["/home/user/coolq","/home/user/zhamao-framework"]

6
docker_mixed/start.sh Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
unzip master.zip
mv zhamao-framework-master/* zhamao-framework/
cd zhamao-framework
php bin/start

View File

@@ -1,141 +0,0 @@
<template>
<div class="doc-chat-container">
<div class="doc-chat-content">
<div v-for="i in chat" v-bind="i">
<div class="doc-chat-row" v-if="i.type === 0">
<div class="doc-chat-box">{{ i.content }}</div>
<img class="doc-chat-avatar" src="https://api.btstu.cn/sjtx/api.php" alt=""/>
</div>
<div class="doc-chat-row doc-chat-row-robot" v-else-if="i.type === 1">
<img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>
<div class="doc-chat-box doc-chat-box-robot">
<span v-for="(p,index) in i.content.split('\n')">{{p}}<br v-if="index !== i.content.length - 1"></span>
</div>
</div>
<div class="doc-chat-row doc-chat-banner" v-else-if="i.type === 2">
{{ i.content }}
</div>
<div class="doc-chat-row doc-chat-row-robot" v-else-if="i.type === 3">
<img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>
<div class="doc-chat-box doc-chat-box-robot">
<img :src="i.content" alt="" />
</div>
</div>
</div>
</div>
</div>
</template>
<!--
type:
0: 我方发送消息
1: 机器人回复消息
2: 显示一个横幅系统消息
3: 机器人回复一个图片
-->
<script>
export default {
name: "ChatBox",
props: ['myChats'],
data() {
return {
chat: this.myChats,
multiline: ''
}
}
}
</script>
<style scoped>
.doc-chat-content {
padding: 12px;
}
.doc-chat-container {
border-radius: 6px;
max-width: 550px;
min-height: 30px;
/*noinspection CssUnresolvedCustomProperty*/
background-color: #f2f4f5;
margin-bottom: 1em;
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
}
.doc-chat-row {
margin: 0;
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
justify-content: flex-end;
}
.doc-chat-banner {
justify-content: center;
background: rgba(0,0,0,0.1);
width: max-content;
margin: 8px auto;
padding: 4px 14px;
border-radius: 8px;
color: gray;
font-size: 14px;
}
.doc-chat-row-robot {
justify-content: flex-start !important;
}
.doc-chat-box {
color: #000000de;
position: relative;
width: fit-content;
max-width: 55%;
border-radius: .5rem;
padding: .4rem .6rem;
margin: .4rem .8rem;
background-color: #fff;
line-height: 1.5;
font-size: 16px;
outline: none;
overflow-wrap: break-word;
white-space: normal;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.doc-chat-box:after {
content: "";
position: absolute;
right: auto;
top: 0;
width: 8px;
height: 12px;
color: #fff;
border: 0 solid transparent;
border-bottom: 7px solid;
border-radius: 0 0 8px 0;
left: calc(100% - 4px);
box-sizing: inherit;
}
.doc-chat-box-robot:after {
content: "";
position: absolute;
right: calc(100% - 4px);
top: 0;
width: 8px;
height: 12px;
color: #fff;
border: 0 solid transparent;
border-bottom: 7px solid;
border-radius: 0 0 0 8px;
left: auto;
box-sizing: inherit;
}
.doc-chat-avatar {
background-color: aquamarine;
width: 36px !important;
height: 36px !important;
border-radius: 18px;
}
</style>

View File

@@ -1,65 +0,0 @@
module.exports = {
title: '炸毛框架 v3',
description: '一个高性能聊天机器人 + Web 框架',
theme: 'antdocs',
markdown: {
lineNumbers: true
},
head: [
['link', { rel: 'icon', href: '/logo_trans.png' }],
['script', {}, `
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?f0f276cefa10aa31a20ae3815a50b795";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
`]
],
themeConfig: {
repo: 'zhamao-robot/zhamao-framework',
logo: '/logo_trans.png',
docsDir: 'docs',
editLinks: true,
lastUpdated: '上次更新',
activeHeaderLinks: false,
nav: [
{ text: '指南', link: '/guide/' },
{ text: '事件', link: '/event/' },
{ text: 'API 文档', link: '/doxy/', target: '_blank' },
{ text: '炸毛框架 v2', link: 'https://docs-v2.zhamao.xin/' }
],
sidebar: {
'/guide/': [
{
title: '指南',
collapsable: false,
sidebarDepth: 1,
children: [
'',
'installation',
'configuration',
'structure',
'get_started',
]
}
],
'/event/': [
{
title: '事件',
collapsable: false,
sidebarDepth: 1,
children: [
'',
'bot',
'http',
'middleware',
'framework',
'extend',
]
}
]
}
}
}

View File

@@ -1 +0,0 @@
framework.zhamao.me

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1 +0,0 @@
User-agent: *

View File

@@ -1 +0,0 @@
@accentColor: #3665aa;

View File

@@ -1 +0,0 @@
docs-v2.zhamao.me

View File

@@ -1,44 +0,0 @@
---
home: true
heroImage: ./logo_trans.png
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 协议兼容的聊天机器人软件的通信,还有数据库连接池、内存缓存、多任务进程等特色,大幅增强性能。
- title: 易于开发
details: 所有功能采用模块化设计,除特殊情况外几乎所有功能都不需要修改框架内任意代码,框架采用灵活的注解进行各类事件绑定,同时支持下断点调试。
- title: 接口直观
details: 支持命令、普通文本、正则匹配、自然语言处理等多种对话解析方式,利用协程巧妙实现了直观的交互式会话模式,同时支持多种富文本的处理。
footer: |
Apache-2.0 Licensed &nbsp;|&nbsp; Copyright © 2019-2022 Zhamao Developer Team &nbsp;|&nbsp; <a href="http://beian.miit.gov.cn">沪ICP备2021010446号-1</a>
---
# 快速上手
## 安装框架和环境
此命令可一键以模板安装框架!(仅限 Linux 和 macOS
```bash
bash <(curl -fsSL https://zhamao.xin/v3.sh)
```
## 运行框架
```bash
cd zhamao-app/
./zhamao server
```
## 效果图
![index_demo](/index_demo.png)

View File

@@ -1,25 +0,0 @@
# 入门
## 事件是什么
简单来说,事件是一个底层的 Event Loop 收到消息后调用对应的方法的一个模型,比如给机器人发送消息后框架会调用你定义的方法来执行你的业务代码。
## 属性和注解是什么
属性Attribute是 PHP 8 最大的新变化之一,是 PHP 官方支持的、内置的注解实现,允许我们通过编程方式获取对应的元数据,可以大大方便我们对某一类代码进行处理。
而注解Annotation则是在 PHP 尚未支持属性的时代,用来代替的社区实现方案,通过解析 PHPDoc 注释来实现自己的注解机制。
炸毛框架同时支持注解和属性,在文档当中,有时会混用两者的字眼,在大多数情况下都可以安全地交换使用,例如 `#[BotEvent]``@BotEvent` 的行为是完全一致的。
## 注解和事件的关系
在炸毛框架中,注解是事件分发的一个重要角色,但注解本身并非事件,更恰当地说,注解代表了事件。
无论是机器人开发过程中场景的 `#[BotCommand]` 或是 HTTP 服务的路由 `#[RequestMapping]` 都是注解代表事件的例子。
## 阻断事件分发
在炸毛框架中,事件由一个统一的事件分发器进行分发,你可以在任意事件中阻断所有后续的分发。
(待考)

View File

@@ -1,86 +0,0 @@
# 机器人事件
<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 | 命令示例 | “” |
> 关于自动帮助生成的更多信息,请参见 【这里链接】
>

View File

@@ -1,3 +0,0 @@
# 扩展事件分发器
TODO

View File

@@ -1,31 +0,0 @@
# 框架事件
<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 进程。
*没有参数*

View File

@@ -1,25 +0,0 @@
# 路由事件
<aside>
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\Http`
</aside>
## Controller
对同一类下的路由进行修饰,只可在类上使用。
| 参数名称 | 允许值 | 用途 | 默认 |
| --- | --- | --- | --- |
| prefix | string | 路由前缀,应用到类下的所有路由 | 必填 |
## Route
路由事件,当对应的路由收到请求时触发。
| 参数名称 | 允许值 | 用途 | 默认 |
| --- | --- | --- | --- |
| route | string | 路由 | 必填 |
| name | string | 路由名称 | “” |
| request_method | array<string> | 允许的请求方法 | [GET, POST] |
| params | array<string, string> | 路由参数 | [] |

View File

@@ -1,18 +0,0 @@
# 中间件事件
<aside>
🛰️ 此页面下的所有注解命名空间为 `ZM\Annotation\Middleware`
</aside>
## Middleware
当绑定了此中间件的方法被触发时触发。
| 参数名称 | 允许值 | 用途 | 默认 |
| --- | --- | --- | --- |
| name | string | 中间件名称 | 必填 |
| params | array<mixed> | 中间件参数 | [] |
> 关于中间件的具体用法,请参见【再来链接】
>

View File

@@ -1,26 +0,0 @@
# 介绍
> 编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
炸毛框架主要面向聊天机器人OneBot 标准)和 API 服务的开发。
框架使用 PHP 编写,无需安装任何额外扩展即可运行在任意主流平台,支持包括 Swoole 和 Workerman 在内的多种驱动,同时支持所有在 OneBot 标准内的通信方式并支持使用注解Annotation和属性Attribute注册绑定各种事件同时引入了依赖注入容器让开发更为便捷。
框架内置了对于 WebSocket 和 HTTP 的服务端和客户端支持,并针对聊天机器人消息处理进行优化扩展,提供常用会话机制和内部调用机制,让代码更为灵活。
## 环境要求
虽然我们已经大力简化了运行框架的要求,但仍然存在少量的必要项:
- PHP 8.0 或以上版本(使用命令 `php -v` 检查)
- Tokenizer 扩展(使用命令 `php -m | grep tokenizer` 检查)
- Composer 工具(使用命令 `composer` 检查)
## 框架特色
- 支持MySQL数据库连接池自带查询缓存提高多查询时的效率
- WebSocket 服务器、HTTP 服务器兼容运行,一个框架多个用处
- 支持命令、自然语言处理等多种插件形式
- 支持多个机器人账号负载均衡
- 完善的插件系统,可以随意加载和编写独立的插件
- 灵活的注释和注解注册事件方式,支持 PHP 原生注解,提示更为友好

View File

@@ -1,47 +0,0 @@
# 配置
炸毛框架的所有配置文件都存储在 `config` 目录中。每个选项都带有文档,所以你可以查阅这些文件并熟悉可用的配置项。
一般来说,我们建议你优先查看 `config/global.php` 文件及其文档,它包含了运行框架所需要的绝大部分配置,例如通信方式和时区等。
## 配置文件格式
除了 `php` 文件外,我们还支持以下格式:
- YAML
- JSON
- TOML
## 环境配置
根据运行的环境采用不同的配置值是很有必要的,例如你可能希望在本地和生产环境中使用不同的数据库配置。
为了方便这一功能,我们提供了基于环境的配置加载措施。你可以在启动框架时使用 `--env` 选项指定当前环境,例如 `./zhamao server --env=production`
如果未明确指定当前环境,则默认环境为 `development`
如果要使某一配置文件只在特定环境下记载,你可以为给文件添加后缀,例如 `example.production.php` 只会在当前环境为 `production` 时加载。
你可以同时设置基础配置文件和环境特定配置文件,例如 `global.php``global.production.php` ,后者将会覆盖前者中的配置项。
### 配置安全
我们**非常不建议**你将数据库信息、访问密钥的敏感字段提交给版本控制,因为这可能会导致相关信息的泄露,同时也不利于其他开发人员修改自己的本地配置。
相反,我们建议你借助 DotEnv 等库,将相关配置信息写入 `.env` 文件中,并按需读取。
## 访问配置项
你可以在任何地方使用全局 `config` 函数获取配置项值。支持使用点语法获取,配置项名称由文件名开头,并允许指定默认值。
```php
# 获取时区,未设置则返回 Asia/Shanghai
$value = config('global.runtime.timezone', 'Asia/Shanghai');
```
你也可以使用 `config` 函数设置配置项,传递数组即可:
```php
# 将时区修改为 UTC
config(['global.runtime.timezone' => 'UTC']);
```

View File

@@ -1,112 +0,0 @@
# 快速上手
在这里,我们将会以一个基础的复读机为例,帮助你快速熟悉框架的开发流程。
在开始之前,你需要先准备一个可用的 OneBot 机器人实现端(客户端)。
其中一些可用的选项为:
- Walle-Q
- 更多…
## 机器人实现端
> 机器人实现端与炸毛框架相互独立。关于实现端本身的问题请向对应的开发者反馈。如果你确定相关问题由炸毛框架引起(例如缺少适配或代码问题等)请向我们报告。
框架支持多种通信方式,这里将以反向 WebSocket 为例,即框架充当 WS 服务端,实现端作为 WS 客户端连接到框架。
这里以 Walle-Q 实现端为例,在实际使用中,你可以自由选用不同的实现端。
你可以前往 Walle-Q 的 [发布页面](https://github.com/onebot-walle/walle-q/releases) 下载最新的发行版本,并运行以进行初始化。
在登录成功后,请关闭 Walle-Q 以修改配置文件。
首次运行后,将会在当前目录下生成 `walle-q.toml` 文件。在大多数场景下,这是你唯一需要接触的文件。
在于框架对接的情况中,我们只需要关注 `onebot.websocket_rev` 配置。
```toml
[onebot]
http = []
http_webhook = []
websocket = []
[[onebot.websocket_rev]]
url = "ws://127.0.0.1:20001" # 这里是框架的监听地址
reconnect_interval = 4
```
修改完成并保存后,重新启动 Walle-Q 并登录即可。如果出现连接失败也请勿惊慌,因为框架此时尚未启动,失败是正常现象。
有些情况可能无法正常扫码登录,可使用 QQ+密码 的方式登录,在最上方插入配置:
```toml
[qq.123456]
password = "MyPassword"
```
> 请将 123456 替换为你的机器人 QQ 号码MyPassword 替换为机器人 QQ 密码。
## 编写第一个功能
在框架中,几乎所有事件的绑定都是通过注解进行的,详情可以参阅 注解的使用。
让我们新建第一个插件,插件的功能很简单,就是复读。我们假设这个复读插件的名字是 `repeater`
```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 可以自由选择位置。
## 启动框架
在保存了上述的代码后,你就可以通过 `./zhamao server` 启动框架了。
启动后Walle-Q 的日志应当会显示连接成功的信息。
此时,你可以通过任意账号向机器人发送 `复读 给我复读` 消息,机器人会回复 `复读 给我复读`
至此,你的第一个功能,复读机,也就开发完成了。
<chat-box :my-chats="[
{type:0,content:'复读 给我复读'},
{type:1,content:'复读 给我复读'},
]"></chat-box>
## 使用机器人 API 和更多事件
如果你希望机器人进行其他复杂的动作(操作),请参见 机器人 API。
关于更多可以注册绑定的事件,请参见 注解事件。

View File

@@ -1,88 +0,0 @@
# 安装
我们希望尽可能轻松地使用炸毛,不论是本地开发或是部署都提供多种选择,尽量覆盖所有需求。
- 一键脚本
- Docker
- Composer
## 一键脚本
炸毛框架提供了一键脚本来设置运行环境并拉取脚手架,帮助你快速进行开发。
如果检测到本机未安装 PHP 或不符合运行要求,脚本将会自动拉取提前编译好的静态 PHP 运行时。
```shell
# 将静态 PHP 和框架安装在当前目录(适用于 Linux、macOS
bash <(curl -fsSL https://zhamao.xin/v3.sh)
# 安装完成后启动
./zhamao server
```
> 关于静态运行时的更多用法,请参见 这里是链接
## Docker
待完善
## Composer
如果你已经有了必要的 PHP 环境和 Composer 工具,你可以直接在任意目录下初始化框架。
> 由于目前 3.0 仍处于预发布阶段,请使用 `composer require zhamao/framework:^3` 来安装
```shell
# 在当前目录初始化框架
composer require zhamao/framework
./vendor/bin/zhamao init
## 安装完成后启动
./zhamao server
## 生成新插件脚手架,用于开发
./zhamao plugin:make
```
## Windows 安装方法
由于 Windows 系统下的 PHP 环境配置较为复杂,我们推荐使用 Docker 或一键脚本来进行安装。
如果你打算在 Windows 使用原生的 Win 环境 PHP你需要先安装 PHP 和 Composer然后在任意目录下执行上方 composer 的安装方法即可。
### 包管理安装
Windows 也可以使用包管理安装 PHP、Composer例如你可以使用 Scoop 包管理进行安装:
```powershell
scoop install php
scoop install composer
```
采用这种包管理安装后,可直接使用 `php``composer` 命令在任意位置,无需配置环境变量。
如果你使用包管理或已经安装了 PHP 到系统内,接下来就直接使用 Composer 来安装框架即可!
```powershell
composer create-project zhamao/framework-starter zhamao-v3
cd zhamao-v3
./zhamao plugin:make
./zhamao server
```
### 纯手动安装
如果你不想使用包管理的方式安装 PHP且让 PHP 仅框架独立使用,你可以依次采用以下的方式来安装 PHP、Composer 和框架:
1. 从 GitHub 下载框架的脚手架,地址:<https://github.com/zhamao-robot/zhamao-framework-starter/archive/refs/heads/master.zip>
2. 解压框架脚手架,重命名文件夹名称为你自己喜欢的名称,例如 `zhamao-v3`
3. 从 PHP 官网下载 PHP选择 `Non Thread Safe` 版本PHP 版本选择 8.0 ~ 8.2 均可(推荐 8.1),下载完成后解压到框架目录下的 `runtime\php` 目录,例如 `D:\zhamao-v3\runtime\php\`
4. 从 [Composer 官网](https://getcomposer.org/download/) 或 [阿里云镜像](https://mirrors.aliyun.com/composer/composer.phar) 下载 Composer下载到 `runtime\` 目录。
5. 在你的脚手架目录下执行 `.\runtime\php\php.exe .\runtime\composer.phar install` 安装框架依赖。
6. 执行框架初始化命令:`./zhamao init`
7. 接下来你就可以使用和上方所有框架操作指令相同的内容了,例如 `./zhamao plugin:make``./zhamao server` 等。
8. 如果你需要使用 Composer你可以使用 `.\runtime\php\php.exe .\runtime\composer.phar` 来代替 `composer` 命令。
## 更多的环境部署和开发方式
除了上述方式之外,框架还支持源码模式、守护进程等运行方式,详情请参阅 [进阶开发]。

View File

@@ -1,36 +0,0 @@
# 目录结构
## 用户目录
### config 目录
`config` 目录包含框架、应用的所有配置文件。最好把这些文件都浏览一遍,并熟悉所有可用的选项。
```
config/
├── global.php # 全局配置文件
├── container.php # 容器配置文件
└── motd.txt # 框架启动时展示的文字信息
```
### vendor 目录
`vendor` 目录包含你通过 Composer 安装的所有依赖,此目录为自动生成,无需操作。
### plugins 目录
`plugins` 目录包含你编写或加载到源代码模式的插件,里面的插件都会被框架自动扫描并解析,你可以在其中利用注解来注册事件绑定并进行相应处理。
比如你通过 `./zhamao plugin:make` 新建了一个名字叫 `test-app` 的插件,并且设置为单文件模式(`file`),那么这个插件内包含的文件及结构为:
```
plugins/
└── test-app/
├── main.php # 你的插件源代码文件
└── zmplugin.json # 插件元信息(如名称、版本等)
```
### zm_data 目录
`zm_data` 目录存放了框架运行时持久化保存的数据,例如 KV 数据库、驱动日志等内容。

View File

@@ -1,34 +0,0 @@
#!/bin/bash
echo "正在检查最新版本的go-cqhttp..."
if [ "$(uname -m)" = "x86_64" ]; then
arch_type="amd64"
elif [ "$(uname -m)" = "i386" ]; then
arch_type="386"
elif [ "$(uname -m)" = "aarch64" ]; then
arch_type="arm64"
else
echo "Not supported architecture: $(uname -m)"
exit 1
fi
aas=$(uname -s | tr 'A-Z' 'a-z')
ver=$(wget -qO- -t1 -T2 "https://fgit-api.zhamao.me/repos/Mrs4s/go-cqhttp/releases" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
if [ "$ver" != "" ]; then
echo "最新版本:"$ver
echo -n "是否下载到本地?[y/N] "
read option
if [ "$option" = "y" ]; then
wget https://fgit.zhamao.me/Mrs4s/go-cqhttp/releases/download/$ver/go-cqhttp-$ver-$aas-$arch_type.tar.gz -O temp.tar.gz
if [ $? != 0 ]; then
wget https://fgit.zhamao.me/Mrs4s/go-cqhttp/releases/download/$ver/go-cqhttp_"$aas""_""$arch_type"".tar.gz" -O temp.tar.gz
fi
tar -zxvf temp.tar.gz go-cqhttp
rm temp.tar.gz
echo "下载完成,启动命令:./go-cqhttp"
echo "首次启动后先编辑config文件"
fi
fi

264
ext/go.sh
View File

@@ -1,264 +0,0 @@
#!/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-app
# 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 ..."
mkdir -p "$ZM_PWD/$ZM_CUSTOM_DIR" && \
cd "$ZM_PWD/$ZM_CUSTOM_DIR" && \
$composer_executable init --name="zhamao/framework-starter" -n -q && \
if_use_aliyun && \
echo "$(nhead) 从 Composer 拉取框架 ..." && \
$composer_executable require -n -q zhamao/framework:^2.7 && \
$composer_executable require -n -q --dev swoole/ide-helper:^4.5 && \
if_restore_native_runtime && \
echo "$(nhead) 初始化框架脚手架文件 ..." && \
vendor/bin/start init >/dev/null 2>&1 && \
$composer_executable dump-autoload -n -q && \
echo "$(nhead) 安装框架完成,已安装到目录 $ZM_CUSTOM_DIR" && \
echo -e "$(nhead) 启动框架命令:""$_cyan""cd $ZM_CUSTOM_DIR && ./zhamao server""$_reset" || {
echo "$(nhead red) 安装框架失败!" && return 1
}
}
# 环境变量设置
test "$ZM_TEMP_DIR" = "" && ZM_TEMP_DIR="/tmp/.zm-runtime"
test "$ZM_CUSTOM_DIR" = "" && ZM_CUSTOM_DIR="zhamao-app"
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

278
ext/v3.sh
View File

@@ -1,278 +0,0 @@
#!/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 不符合要求,是否下载安装独立的内建 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"
ver_id=$($php_executable -r "echo PHP_VERSION_ID;")
if [ "$ver_id" -lt 80000 ]; then
echo "$(nhead red) PHP 版本过低,框架需要 PHP >= 8.0.0" && \
prompt_install_native_php && \
install_native_php && \
install_native_composer && composer_check && return 0 || return 1
fi
if [ "$($php_executable -m | grep tokenizer)" = "" ]; then
echo "$(nhead red) PHP 不存在 tokenizer 扩展,可能无法正常使用框架!" && \
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 zhamao/framework:^3 && \
$composer_executable require -n --dev swoole/ide-helper:^4.5 && \
if_restore_native_runtime && \
echo "$(nhead) 初始化框架脚手架文件 ..." && \
vendor/bin/zhamao init && \
$composer_executable dump-autoload -n && \
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

View File

@@ -1,69 +0,0 @@
#!/usr/bin/env bash
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 $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 "失败!请检查网络连接!"
rm -rf "$2"
return 1
fi
return 0
}
function test_composer_and_php() {
succ=$("$(pwd)/runtime/composer" -n about | grep Manage)
if [ "$succ" = "" ]; then
echo "Download PHP binary and composer failed!"
return 1
fi
return 0
}
if [ "$(uname -s)" != "Linux" ]; then
echo "Only support Linux!!!"
exit 1
fi
ZM_PHP_VERSION="7.4"
if [ "$ZM_DOWN_PHP_VERSION" != "" ]; then
ZM_PHP_VERSION="$ZM_DOWN_PHP_VERSION"
echo "Using custom PHP version: $ZM_PHP_VERSION"
fi
mkdir "$(pwd)/runtime" >/dev/null 2>&1
if [ ! -f "$(pwd)/runtime/php" ]; then
download_file "https://dl.zhamao.me/php-bin/down.php?php_ver=$ZM_PHP_VERSION&arch=$(uname -m)" "$(pwd)/runtime/php.tar.gz"
if [ $? -ne 0 ]; then
exit 1
fi
tar -xf "$(pwd)/runtime/php.tar.gz" -C "$(pwd)/runtime/"
fi
if [ ! -f "$(pwd)/runtime/composer" ]; then
download_file "https://mirrors.aliyun.com/composer/composer.phar" "$(pwd)/runtime/composer.phar"
if [ $? -ne 0 ]; then
exit 1
fi
echo '$(dirname $0)/php $(dirname $0)/composer.phar $@' > $(pwd)/runtime/composer
chmod +x $(pwd)/runtime/composer
test_composer_and_php
fi
if [ $? -ne 0 ]; then
exit 1
fi
echo "成功下载!" && \
echo -e "PHP使用\truntime/php -v" && \
echo -e "Composer使用\truntime/composer"

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
$plugin = new \ZM\ZMApplication(__DIR__);
/*
* 发送 "测试 123",回复 "你好123"
*/
$cmd1 = BotCommand::make('test', '测试')->withArgument('arg1')->on(fn () => '你好,{{arg1}}');
/*
* 浏览器访问 http://ip:port/index233返回内容
*/
$route1 = Route::make('/index233')->on(fn () => '<h1>Hello world</h1>');
$plugin->addBotCommand($cmd1);
$plugin->addHttpRoute($route1);
$plugin->run();

View File

@@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
use OneBot\Driver\Event\Http\HttpRequestEvent;
require 'vendor/autoload.php';
// 创建框架 App
$app = new ZM\ZMApplication();
// 传入自定义配置文件
$app->patchConfig([
'driver' => 'workerman',
]);
// 改变启动所需的参数
$app->patchArgs([
'--private-mode',
]);
// 如果有 Composer 依赖的插件,使用 enablePlugins 进行开启
$app->enablePlugins([
'a',
'b',
'c',
'd',
]);
// BotCommand 事件构造
$cmd = BotCommand::make('test')->on(function () {
ctx()->reply('test ok');
});
$event = BotEvent::make(type: 'message')->on(function () {
});
$app->addBotEvent($event);
$app->addBotCommand($cmd);
$app->addEvent(HttpRequestEvent::getName(), function (HttpRequestEvent $event) {
$event->withResponse(\OneBot\Http\HttpFactory::getInstance()->createResponse(503));
});
$app->run();

View File

@@ -1,10 +0,0 @@
{
"dependencies": {
"vuepress-theme-antdocs": "^1.4.5",
"vuepress": "^1.9.7"
},
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}
}

85
phar-starter.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
global $is_phar;
use Framework\FrameworkLoader;
$is_phar = true;
if (substr(__DIR__, 0, 7) != 'phar://') {
die("You can not run this script directly!\n");
}
testEnvironment();
spl_autoload_register(function ($class) {
//echo $class."\n";
$exp = str_replace("\\", '/', $class);
$exp = __DIR__ . '/src/' . $exp . '.php';
if (is_file($exp)) {
require_once $exp;
}
});
loadPhp(__DIR__ . '/src');
Swoole\Coroutine::set([
'max_coroutine' => 30000,
]);
date_default_timezone_set("Asia/Shanghai");
define('WORKING_DIR', __DIR__);
define('FRAMEWORK_DIR', __DIR__);
define('LOAD_MODE', 2);
$s = new FrameworkLoader($argv);
function loadPhp($dir) {
$dirs = scandir($dir);
foreach ($dirs as $v) {
$path = $dir . '/' . $v;
if (is_dir($path)) {
loadPhp($path);
} else {
if (pathinfo($dir . '/' . $v)['extension'] == 'php') {
if(pathinfo($dir . '/' . $v)['basename'] == 'terminal_listener.php') continue;
//echo 'loading '.$path.PHP_EOL;
require_once $path;
}
}
}
}
function testEnvironment() {
$current_dir = realpath('.');
@mkdir($current_dir . '/config/');
if (!is_file($current_dir . '/config/global.php')) {
echo "Exporting default global config...\n";
$global = file_get_contents(__DIR__ . '/config/global.php');
$global = str_replace("WORKING_DIR", 'realpath(__DIR__ . "/../")', $global);
file_put_contents($current_dir . '/config/global.php', $global);
}
if (!is_file($current_dir . '/config/file_header.json')) {
echo "Exporting default file_header config...\n";
$global = file_get_contents(__DIR__ . '/config/file_header.json');
file_put_contents($current_dir . '/config/file_header.json', $global);
}
if (!is_dir($current_dir . '/resources')) mkdir($current_dir . '/resources');
if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src');
if (!is_dir($current_dir . '/src')) mkdir($current_dir . '/src');
if (!is_dir($current_dir . '/src/Module')) {
mkdir($current_dir . '/src/Module');
mkdir($current_dir . '/src/Module/Example');
file_put_contents($current_dir . '/src/Module/Example/Hello.php', file_get_contents(__DIR__ . '/tmp/Hello.php.bak'));
mkdir($current_dir . '/src/Module/Middleware');
file_put_contents($current_dir . '/src/Module/Middleware/TimerMiddleware.php', file_get_contents(__DIR__ . '/tmp/TimerMiddleware.php.bak'));
}
if (!is_dir($current_dir . '/src/Custom')) {
mkdir($current_dir . '/src/Custom');
mkdir($current_dir . '/src/Custom/Annotation');
mkdir($current_dir . '/src/Custom/Connection');
file_put_contents($current_dir . '/src/Custom/global_function.php', "<?php\n\n//这里写你的全局方法");
}
}

View File

@@ -1,22 +0,0 @@
parameters:
reportUnmatchedIgnoredErrors: false
level: 4
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#'
- '#Unsafe usage of new static#'
- '#Call to method initTableList\(\) of deprecated class ZM\\DB\\DB#'
- '#class Fiber#'
- # Ignore Prophesize deprecation bug: https://github.com/phpstan/phpstan-deprecation-rules/issues/76
message: '#^Call to deprecated method prophesize\(\) of class Tests\\TestCase#'
path: tests
dynamicConstantNames:
- SWOOLE_VERSION
- ZM_TEST_LOG_DEBUG
- _PHAR_STUB_ID
- LOAD_MODE

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./tests/bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertDeprecationsToExceptions="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
testdox="true"
verbose="true"
>
<testsuites>
<testsuite name="Zhamao Framework Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./src/ZM</directory>
</include>
<report>
<html outputDirectory="./build/html-coverage"/>
<clover outputFile="./build/coverage.xml"/>
</report>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
</php>
</phpunit>

View File

@@ -1,10 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example page</title>
</head>
<body>
<div style="background: red; width: 100px; height: 100px"></div>
</body>
</html>

BIN
resources/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

BIN
resources/images/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,19 @@
<?php
namespace Custom\Annotation;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\Interfaces\CustomAnnotation;
/**
* Class CustomAnnotation
* @Annotation
* @Target("ALL")
* @package Custom\Annotation
*/
class Example implements CustomAnnotation
{
/** @var string */
public $str;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Custom\Connection;
use ZM\Connection\WSConnection;
class CustomConnection extends WSConnection
{
public function getType() {
return "custom";
}
}

View File

@@ -0,0 +1,3 @@
<?php
//这里写你的全局函数

277
src/Framework/Console.php Executable file
View File

@@ -0,0 +1,277 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2018/2/10
* Time: 下午6:13
*/
namespace Framework;
use ZM\Annotation\Swoole\SwooleEventAt;
use ZM\Connection\WSConnection;
use ZM\Utils\ZMUtil;
use Exception;
class Console
{
/**
* @var false|resource
*/
public static $console_proc = null;
public static $pipes = [];
static function setColor($string, $color = "") {
switch ($color) {
case "red":
return "\x1b[38;5;203m" . $string . "\x1b[m";
case "green":
return "\x1b[38;5;83m" . $string . "\x1b[m";
case "yellow":
return "\x1b[38;5;227m" . $string . "\x1b[m";
case "blue":
return "\033[34m" . $string . "\033[0m";
case "pink": // I really don't know what stupid color it is.
case "lightpurple":
return "\x1b[38;5;207m" . $string . "\x1b[m";
case "lightblue":
return "\x1b[38;5;87m" . $string . "\x1b[m";
case "gold":
return "\x1b[38;5;214m" . $string . "\x1b[m";
case "gray":
return "\x1b[38;5;59m" . $string . "\x1b[m";
case "lightlightblue":
return "\x1b[38;5;63m" . $string . "\x1b[m";
default:
return $string;
}
}
static function error($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[E] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "red") . "\n");
}
static function warning($obj, $head = null) {
if ($head === null) $head = date("[H:i:s]") . " [W] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 1) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "blue" : "yellow") . "\n");
}
}
static function info($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[I] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 2) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, in_array("--white-term", FrameworkLoader::$argv) ? "black" : "lightblue") . "\n");
}
}
static function success($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[S] ";
if (ZMBuf::$info_level !== null && in_array(ZMBuf::$info_level->get(), [1, 2])) {
$trace = debug_backtrace()[1] ?? ['file' => '', 'function' => ''];
$trace = "[" . basename($trace["file"], ".php") . ":" . $trace["function"] . "] ";
}
if (ZMBuf::$atomics["info_level"]->get() >= 2) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "green") . "\n");
}
}
static function verbose($obj, $head = null) {
if ($head === null) $head = date("[H:i:s] ") . "[V] ";
if (ZMBuf::$atomics["info_level"]->get() >= 3) {
if (!is_string($obj)) {
if (isset($trace)) {
var_dump($obj);
return;
} else $obj = "{Object}";
}
echo(self::setColor($head . ($trace ?? "") . $obj, "blue") . "\n");
}
}
static function debug($msg) {
if (ZMBuf::$atomics["info_level"]->get() >= 4) Console::log(date("[H:i:s] ") . "[D] " . $msg, 'gray');
}
static function log($obj, $color = "") {
if (!is_string($obj)) var_dump($obj);
else echo(self::setColor($obj, $color) . "\n");
}
static function stackTrace() {
$log = "Stack trace:\n";
$trace = debug_backtrace();
//array_shift($trace);
foreach ($trace as $i => $t) {
if (!isset($t['file'])) {
$t['file'] = 'unknown';
}
if (!isset($t['line'])) {
$t['line'] = 0;
}
if (!isset($t['function'])) {
$t['function'] = 'unknown';
}
$log .= "#$i {$t['file']}({$t['line']}): ";
if (isset($t['object']) and is_object($t['object'])) {
$log .= get_class($t['object']) . '->';
}
$log .= "{$t['function']}()\n";
}
$log = Console::setColor($log, "gray");
echo $log;
}
static function listenConsole() {
if (in_array('--disable-console-input', FrameworkLoader::$argv) || in_array('--debug-mode', FrameworkLoader::$argv)) {
self::info("ConsoleCommand disabled.");
return;
}
global $terminal_id;
global $port;
$port = ZMBuf::globals("port");
$vss = new SwooleEventAt();
$vss->type = "open";
$vss->level = 256;
$vss->rule = "connectType:terminal";
$terminal_id = call_user_func(function () {
try {
$data = random_bytes(16);
} catch (Exception $e) {
return "";
}
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return strtoupper(vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)));
});
$vss->callback = function(?WSConnection $conn) use ($terminal_id){
$req = ctx()->getRequest();
if($conn->getType() != "terminal") return false;
Console::debug("Terminal fd: ".$conn->fd);
if(($req->header["x-terminal-id"] ?? "") != $terminal_id) {
$conn->close();
return false;
}
return false;
};
ZMBuf::$events[SwooleEventAt::class][] = $vss;
$vss2 = new SwooleEventAt();
$vss2->type = "message";
$vss2->rule = "connectType:terminal";
$vss2->callback = function(?WSConnection $conn){
if ($conn === null) return false;
if($conn->getType() != "terminal") return false;
$cmd = ctx()->getFrame()->data;
self::executeCommand($cmd);
return false;
};
ZMBuf::$events[SwooleEventAt::class][] = $vss2;
go(function () {
global $terminal_id, $port;
$descriptorspec = array(
0 => STDIN,
1 => STDOUT,
2 => STDERR
);
self::$console_proc = proc_open('php -r \'$terminal_id = "'.$terminal_id.'";$port = '.$port.';require "'.__DIR__.'/terminal_listener.php";\'', $descriptorspec, $pipes);
});
}
/**
* @param string $cmd
* @return bool
*/
private static function executeCommand(string $cmd) {
$it = explodeMsg($cmd);
switch ($it[0] ?? '') {
case 'logtest':
Console::log(date("[H:i:s]") . " [L] This is normal msg. (0)");
Console::error("This is error msg. (0)");
Console::warning("This is warning msg. (1)");
Console::info("This is info msg. (2)");
Console::success("This is success msg. (2)");
Console::verbose("This is verbose msg. (3)");
Console::debug("This is debug msg. (4)");
return true;
case 'call':
$class_name = $it[1];
$function_name = $it[2];
$class = new $class_name([]);
call_user_func_array([$class, $function_name], []);
return true;
case 'bc':
$code = base64_decode($it[1] ?? '', true);
try {
eval($code);
} catch (Exception $e) {
}
return true;
case 'echo':
Console::info($it[1]);
return true;
case 'color':
Console::log($it[2], $it[1]);
return true;
case 'stop':
ZMUtil::stop();
return false;
case 'reload':
case 'r':
ZMUtil::reload();
return false;
case 'save':
$origin = ZMBuf::$atomics["info_level"]->get();
//ZMBuf::$atomics["info_level"]->set(3);
DataProvider::saveBuffer();
//ZMBuf::$atomics["info_level"]->set($origin);
return true;
case '':
return true;
default:
Console::info("Command not found: " . $cmd);
return true;
}
}
public static function withSleep(string $string, int $int) {
self::info($string);
sleep($int);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Framework;
use ZM\Annotation\Swoole\OnSave;
class DataProvider
{
public static $buffer_list = [];
public static function getResourceFolder() {
return self::getWorkingDir() . '/resources/';
}
public static function getWorkingDir() {
if(LOAD_MODE == 0) return WORKING_DIR;
elseif (LOAD_MODE == 1) return LOAD_MODE_COMPOSER_PATH;
elseif (LOAD_MODE == 2) return realpath('.');
return null;
}
public static function getDataConfig() {
return CONFIG_DIR;
}
public static function addSaveBuffer($buf_name, $sub_folder = null) {
$name = ($sub_folder ?? "") . "/" . $buf_name . ".json";
self::$buffer_list[$buf_name] = $name;
Console::debug("Added " . $buf_name . " at $sub_folder");
ZMBuf::set($buf_name, self::getJsonData($name));
}
public static function saveBuffer() {
$head = Console::setColor(date("[H:i:s] ") . "[V] Saving buffer......", "blue");
if (ZMBuf::$atomics["info_level"]->get() >= 3)
echo $head;
foreach (self::$buffer_list as $k => $v) {
Console::debug("Saving " . $k . " to " . $v);
self::setJsonData($v, ZMBuf::get($k));
}
foreach (ZMBuf::$events[OnSave::class] ?? [] as $v) {
$c = $v->class;
$method = $v->method;
$class = new $c();
Console::debug("Calling @OnSave: $c -> $method");
$class->$method();
}
if (ZMBuf::$atomics["info_level"]->get() >= 3)
echo Console::setColor("saved", "blue") . PHP_EOL;
}
public static function getFrameworkLink() {
return ZMBuf::globals("http_reverse_link");
}
public static function getJsonData(string $string) {
if (!file_exists(self::getDataConfig() . $string)) return [];
return json_decode(file_get_contents(self::getDataConfig() . $string), true);
}
public static function setJsonData($filename, array $args) {
$pathinfo = pathinfo($filename);
if (!is_dir(self::getDataConfig() . $pathinfo["dirname"])) {
Console::debug("Making Directory: " . self::getDataConfig() . $pathinfo["dirname"]);
mkdir(self::getDataConfig() . $pathinfo["dirname"]);
}
$r = file_put_contents(self::getDataConfig() . $filename, json_encode($args, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING));
if ($r === false) {
Console::warning("无法保存文件: " . $filename);
}
}
public static function getDataFolder() {
return ZM_DATA;
}
}

View File

@@ -0,0 +1,213 @@
<?php
namespace Framework;
use Doctrine\Common\Annotations\AnnotationReader;
use ReflectionClass;
use ReflectionMethod;
use Swoole\Runtime;
use ZM\Annotation\Swoole\OnEvent;
use Exception;
use Swoole\WebSocket\Server;
/**
* Class FrameworkLoader
* Everything is beginning from here
* @package Framework
*/
class FrameworkLoader
{
/** @var GlobalConfig */
public static $settings;
/** @var FrameworkLoader|null */
public static $instance = null;
/** @var float|string */
public static $run_time;
/**
* @var array
*/
public static $argv;
/** @var Server */
private $server;
public function __construct($args = [])
{
$this->requireGlobalFunctions();
if (LOAD_MODE == 0) define("WORKING_DIR", getcwd());
elseif (LOAD_MODE == 1) define("WORKING_DIR", realpath(__DIR__ . "/../../"));
elseif (LOAD_MODE == 2) echo "Phar mode: " . WORKING_DIR . PHP_EOL;
//$this->registerAutoloader('classLoader');
require_once "DataProvider.php";
if (file_exists(DataProvider::getWorkingDir() . "/vendor/autoload.php")) {
/** @noinspection PhpIncludeInspection */
require_once DataProvider::getWorkingDir() . "/vendor/autoload.php";
}
if (LOAD_MODE == 0) {
echo "* This is repository mode.\n";
$composer = json_decode(file_get_contents(DataProvider::getWorkingDir() . "/composer.json"), true);
if (!isset($composer["autoload"]["psr-4"]["Module\\"])) {
echo "框架源码模式需要在autoload文件中添加Module目录为自动加载是否添加[Y/n] ";
$r = strtolower(trim(fgets(STDIN)));
if ($r === "" || $r === "y") {
$composer["autoload"]["psr-4"]["Module\\"] = "src/Module";
$composer["autoload"]["psr-4"]["Custom\\"] = "src/Custom";
$r = file_put_contents(DataProvider::getWorkingDir() . "/composer.json", json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
if ($r !== false) {
echo "成功添加!请重新进行 composer update \n";
exit(1);
} else {
echo "添加失败!请按任意键继续!";
fgets(STDIN);
exit(1);
}
} else {
exit(1);
}
}
}
if (LOAD_MODE == 2) {
require_once FRAMEWORK_DIR . "/vendor/autoload.php";
spl_autoload_register('phar_classloader');
}
self::$settings = new GlobalConfig();
if (self::$settings->get("debug_mode") === true) {
$args[] = "--debug-mode";
$args[] = "--disable-console-input";
}
self::$argv = $args;
if (!in_array("--debug-mode", self::$argv)) {
Runtime::enableCoroutine(true, SWOOLE_HOOK_ALL);
}
self::$settings = new GlobalConfig();
ZMBuf::$globals = self::$settings;
if (!self::$settings->success) die("Failed to load global config. Please check config/global.php file");
$this->defineProperties();
//start swoole Framework
$this->selfCheck();
try {
$this->server = new Server(self::$settings->get("host"), self::$settings->get("port"));
$settings = self::$settings->get("swoole");
if (in_array("--daemon", $args)) {
$settings["daemonize"] = 1;
Console::log("已启用守护进程,输出重定向到 " . $settings["log_file"]);
self::$argv[] = "--disable-console-input";
}
$this->server->set($settings);
$all_event_class = self::$settings->get("server_event_handler_class") ?? [];
if (!in_array(ServerEventHandler::class, $all_event_class)) {
$all_event_class[] = ServerEventHandler::class;
}
$event_list = [];
foreach ($all_event_class as $v) {
$reader = new AnnotationReader();
$reflection_class = new ReflectionClass($v);
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $vs) {
$method_annotations = $reader->getMethodAnnotations($vs);
if ($method_annotations != []) {
$annotation = $method_annotations[0];
if ($annotation instanceof OnEvent) {
$annotation->class = $v;
$annotation->method = $vs->getName();
$event_list[strtolower($annotation->event)] = $annotation;
}
}
}
}
foreach ($event_list as $k => $v) {
$this->server->on($k, function (...$param) use ($v) {
$c = $v->class;
//echo $c.PHP_EOL;
$c = new $c();
call_user_func_array([$c, $v->method], $param);
});
}
ZMBuf::initAtomic();
if (in_array("--remote-shell", $args)) RemoteShell::listen($this->server, "127.0.0.1");
if (in_array("--log-error", $args)) ZMBuf::$atomics["info_level"]->set(0);
if (in_array("--log-warning", $args)) ZMBuf::$atomics["info_level"]->set(1);
if (in_array("--log-info", $args)) ZMBuf::$atomics["info_level"]->set(2);
if (in_array("--log-verbose", $args)) ZMBuf::$atomics["info_level"]->set(3);
if (in_array("--log-debug", $args)) ZMBuf::$atomics["info_level"]->set(4);
Console::log(
"host: " . self::$settings->get("host") .
", port: " . self::$settings->get("port") .
", log_level: " . ZMBuf::$atomics["info_level"]->get() .
", version: " . ZM_VERSION .
"\nworking_dir: " . DataProvider::getWorkingDir()
);
global $motd;
if (!file_exists(DataProvider::getWorkingDir() . "/config/motd.txt")) {
echo $motd;
} else {
echo file_get_contents(DataProvider::getWorkingDir() . "/config/motd.txt");
}
if (in_array("--debug-mode", self::$argv))
Console::warning("You are in debug mode, do not use in production!");
$this->server->start();
} catch (Exception $e) {
Console::error("Framework初始化出现错误请检查");
Console::error($e->getMessage());
die;
}
}
private function requireGlobalFunctions()
{
require_once __DIR__ . '/global_functions.php';
}
private function defineProperties()
{
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", self::$settings->get("zm_data"));
define("ZM_VERSION", json_decode(file_get_contents(__DIR__ . "/../../composer.json"), true)["version"] ?? "unknown");
define("CONFIG_DIR", self::$settings->get("config_dir"));
define("CRASH_DIR", self::$settings->get("crash_dir"));
@mkdir(ZM_DATA);
@mkdir(CONFIG_DIR);
@mkdir(CRASH_DIR);
define("ZM_MATCH_ALL", 0);
define("ZM_MATCH_FIRST", 1);
define("ZM_MATCH_NUMBER", 2);
define("ZM_MATCH_SECOND", 3);
define("ZM_BREAKPOINT", 'if(in_array("--debug-mode", \Framework\FrameworkLoader::$argv)) extract(\Psy\debug(get_defined_vars(), isset($this) ? $this : @get_called_class()));');
define("BP", ZM_BREAKPOINT);
define("ZM_DEFAULT_FETCH_MODE", self::$settings->get("sql_config")["sql_default_fetch_mode"] ?? 4);
}
private function selfCheck()
{
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
//if (!extension_loaded("gd")) die("Can not find gd extension.\n");
//if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (!function_exists("ctype_alpha")) die("Can not find ctype extension.\n");
if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n");
if (substr(PHP_VERSION, 0, 1) < "7") die("PHP >=7 required.\n");
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
return true;
}
}
global $motd;
$motd = <<<EOL
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
EOL;

View File

@@ -0,0 +1,37 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2019-03-16
* Time: 13:58
*/
namespace Framework;
/**
* 请不要diss此class的语法。可能写的很糟糕。
* Class GlobalConfig
*/
class GlobalConfig
{
private $config = null;
public $success = false;
public function __construct() {
/** @noinspection PhpIncludeInspection */
include_once DataProvider::getWorkingDir() . '/config/global.php';
global $config;
$this->success = true;
$this->config = $config;
}
public function get($key) {
$r = $this->config[$key] ?? null;
if ($r === null) return null;
return $r;
}
public function getAll() {
return $this->config;
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace Framework;
use Co;
use Exception;
use Swoole\Coroutine;
use swoole\server;
class RemoteShell
{
const STX = "DEBUG";
private static $contexts = array();
static $oriPipeMessageCallback = null;
/**
* @var server
*/
static $serv;
static $menu = array(
"p|print [variant]\t打印一个PHP变量的值",
"e|exec [code]\t执行一段PHP代码",
"w|worker [id]\t切换Worker进程",
"l|list\t打印服务器所有连接的fd",
"s|stats\t打印服务器状态",
"c|coros\t打印协程列表",
"b|bt\t打印协程调用栈",
"i|info [fd]\t显示某个连接的信息",
"h|help\t显示帮助界面",
"q|quit\t退出终端",
);
const PAGESIZE = 20;
/**
* @param $serv server
* @param string $host
* @param int $port
* @throws Exception
* @throws Exception
*/
static function listen($serv, $host = "127.0.0.1", $port = 9599) {
Console::warning("正在监听".$host.":".strval($port)."的调试接口,请注意安全");
$port = $serv->listen($host, $port, SWOOLE_SOCK_TCP);
if (!$port) {
throw new Exception("listen fail.");
}
$port->set(array(
"open_eof_split" => true,
'package_eof' => "\r\n",
));
$port->on("Connect", array(__CLASS__, 'onConnect'));
$port->on("Close", array(__CLASS__, 'onClose'));
$port->on("Receive", array(__CLASS__, 'onReceive'));
if (method_exists($serv, 'getCallback')) {
self::$oriPipeMessageCallback = $serv->getCallback('PipeMessage');
}
$serv->on("PipeMessage", array(__CLASS__, 'onPipeMessage'));
self::$serv = $serv;
}
static function onConnect($serv, $fd, $reactor_id) {
self::$contexts[$fd]['worker_id'] = $serv->worker_id;
self::output($fd, implode("\r\n", self::$menu));
}
static function output($fd, $msg) {
if (!isset(self::$contexts[$fd]['worker_id'])) {
$msg .= "\r\nworker#" . self::$serv->worker_id . "$ ";
} else {
$msg .= "\r\nworker#" . self::$contexts[$fd]['worker_id'] . "$ ";
}
self::$serv->send($fd, $msg);
}
static function onClose($serv, $fd, $reactor_id) {
unset(self::$contexts[$fd]);
}
static function onPipeMessage($serv, $src_worker_id, $message) {
//不是 debug 消息
if (!is_string($message) or substr($message, 0, strlen(self::STX)) != self::STX) {
if (self::$oriPipeMessageCallback == null) {
trigger_error("require swoole-4.3.0 or later.", E_USER_WARNING);
return true;
}
return call_user_func(self::$oriPipeMessageCallback, $serv, $src_worker_id, $message);
} else {
$request = unserialize(substr($message, strlen(self::STX)));
self::call($request['fd'], $request['func'], $request['args']);
}
return true ;
}
static protected function call($fd, $func, $args) {
ob_start();
call_user_func_array($func, $args);
self::output($fd, ob_get_clean());
}
static protected function exec($fd, $func, $args) {
//不在当前Worker进程
if (self::$contexts[$fd]['worker_id'] != self::$serv->worker_id) {
self::$serv->sendMessage(self::STX . serialize(['fd' => $fd, 'func' => $func, 'args' => $args]), self::$contexts[$fd]['worker_id']);
} else {
self::call($fd, $func, $args);
}
}
static function getCoros() {
var_export(iterator_to_array(Coroutine::listCoroutines()));
}
static function getBackTrace($_cid) {
$info = Co::getBackTrace($_cid);
if (!$info) {
echo "coroutine $_cid not found.";
} else {
echo get_debug_print_backtrace($info);
}
}
static function printVariant($var) {
$var = ltrim($var, '$ ');
var_dump($var);
var_dump($$var);
}
static function evalCode($code) {
eval($code . ';');
}
/**
* @param $serv server
* @param $fd
* @param $reactor_id
* @param $data
*/
static function onReceive($serv, $fd, $reactor_id, $data) {
$args = explode(" ", $data, 2);
$cmd = trim($args[0]);
unset($args[0]);
switch ($cmd) {
case 'w':
case 'worker':
if (!isset($args[1])) {
self::output($fd, "invalid command.");
break;
}
$dstWorkerId = intval($args[1]);
self::$contexts[$fd]['worker_id'] = $dstWorkerId;
self::output($fd, "[switching to worker " . self::$contexts[$fd]['worker_id'] . "]");
break;
case 'e':
case 'exec':
if (!isset($args[1])) {
self::output($fd, "invalid command.");
break;
}
$var = trim($args[1]);
self::exec($fd, 'self::evalCode', [$var]);
break;
case 'p':
case 'print':
$var = trim($args[1]);
self::exec($fd, 'self::printVariant', [$var]);
break;
case 'h':
case 'help':
self::output($fd, implode("\r\n", self::$menu));
break;
case 's':
case 'stats':
$stats = $serv->stats();
self::output($fd, var_export($stats, true));
break;
case 'c':
case 'coros':
self::exec($fd, 'self::getCoros', []);
break;
/**
* 查看协程堆栈
*/
case 'bt':
case 'b':
case 'backtrace':
if (empty($args[1])) {
self::output($fd, "invalid command [" . trim($args[1]) . "].");
break;
}
$_cid = intval($args[1]);
self::exec($fd, 'self::getBackTrace', [$_cid]);
break;
case 'i':
case 'info':
if (empty($args[1])) {
self::output($fd, "invalid command [" . trim($args[1]) . "].");
break;
}
$_fd = intval($args[1]);
$info = $serv->getClientInfo($_fd);
if (!$info) {
self::output($fd, "connection $_fd not found.");
} else {
self::output($fd, var_export($info, true));
}
break;
case 'l':
case 'list':
$tmp = array();
foreach ($serv->connections as $fd) {
$tmp[] = $fd;
if (count($tmp) > self::PAGESIZE) {
self::output($fd, json_encode($tmp));
$tmp = array();
}
}
if (count($tmp) > 0) {
self::output($fd, json_encode($tmp));
}
break;
case 'q':
case 'quit':
$serv->close($fd);
break;
default:
self::output($fd, "unknow command[$cmd]");
break;
}
}
}
function get_debug_print_backtrace($traces) {
$ret = array();
foreach ($traces as $i => $call) {
$object = '';
if (isset($call['class'])) {
$object = $call['class'] . $call['type'];
if (is_array($call['args'])) {
foreach ($call['args'] as &$arg) {
get_arg($arg);
}
}
}
$ret[] = '#' . str_pad($i, 3, ' ')
. $object . $call['function'] . '(' . implode(', ', $call['args'])
. ') called at [' . $call['file'] . ':' . $call['line'] . ']';
}
return implode("\n", $ret);
}
function get_arg(&$arg) {
if (is_object($arg)) {
$arr = (array)$arg;
$args = array();
foreach ($arr as $key => $value) {
if (strpos($key, chr(0)) !== false) {
$key = ''; // Private variable found
}
$args[] = '[' . $key . '] => ' . get_arg($value);
}
$arg = get_class($arg) . ' Object (' . implode(',', $args) . ')';
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Framework;
use Co;
use Doctrine\Common\Annotations\AnnotationException;
use Swoole\Http\Request;
use Swoole\Server;
use Swoole\WebSocket\Frame;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnEvent;
use ZM\Connection\ConnectionManager;
use ZM\Event\EventHandler;
use ZM\Http\Response;
class ServerEventHandler
{
/**
* @OnEvent("WorkerStart")
* @param Server $server
* @param $worker_id
* @throws AnnotationException
* @throws \ReflectionException
*/
public function onWorkerStart(Server $server, $worker_id) {
if ($server->taskworker === false) {
FrameworkLoader::$run_time = microtime(true);
EventHandler::callSwooleEvent("WorkerStart", $server, $worker_id);
} else {
ob_start();
AnnotationParser::registerMods();
//加载Custom目录下的自定义的内部类
ConnectionManager::registerCustomClass();
ob_get_clean();
}
}
/**
* @OnEvent("message")
* @param $server
* @param Frame $frame
* @throws AnnotationException
*/
public function onMessage($server, Frame $frame) {
Console::debug("Calling Swoole \"message\" from fd=" . $frame->fd);
EventHandler::callSwooleEvent("message", $server, $frame);
}
/**
* @OnEvent("request")
* @param $request
* @param $response
* @throws AnnotationException
*/
public function onRequest($request, $response) {
$response = new Response($response);
Console::debug("Receiving Http request event, cid=" . Co::getCid());
EventHandler::callSwooleEvent("request", $request, $response);
}
/**
* @OnEvent("open")
* @param $server
* @param Request $request
* @throws AnnotationException
*/
public function onOpen($server, Request $request) {
Console::debug("Calling Swoole \"open\" event from fd=" . $request->fd);
EventHandler::callSwooleEvent("open", $server, $request);
}
/**
* @OnEvent("close")
* @param $server
* @param $fd
* @throws AnnotationException
*/
public function onClose($server, $fd) {
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
EventHandler::callSwooleEvent("close", $server, $fd);
}
}

121
src/Framework/ZMBuf.php Executable file
View File

@@ -0,0 +1,121 @@
<?php
/**
* Created by PhpStorm.
* User: jerry
* Date: 2018/2/25
* Time: 下午11:11
*/
namespace Framework;
use Swoole\Atomic;
use Swoole\Database\PDOPool;
use swoole_atomic;
use ZM\connection\WSConnection;
class ZMBuf
{
//读写的缓存数据需要在worker_num = 1下才能正常使用
/** @var mixed[] ZMBuf的data */
private static $cache = [];
/** @var WSConnection[] */
static $connect = [];//储存连接实例的数组
//Scheduler计划任务连接实例只可以在单worker_num时使用
static $scheduler = null; //This is stupid warning...
//Swoole SQL连接池多进程下每个进程一个连接池
/** @var PDOPool */
static $sql_pool = null;//保存sql连接池的类
//只读的数据可以在多worker_num下使用
/** @var null|\Framework\GlobalConfig */
static $globals = null;
// swoole server操作对象每个进程均分配
/** @var \swoole_websocket_server $server */
static $server;
/** @var array Http请求uri路径根节点 */
public static $req_mapping_node;
/** @var mixed TimeNLP初始化后的对象每个进程均可初始化 */
public static $time_nlp;
/** @var string[] $custom_connection_class */
public static $custom_connection_class = [];//保存自定义的ws connection连接类型的
// Atomic可跨进程读写的原子计数任何地方均可使用
/** @var null|swoole_atomic */
static $info_level = null;//保存log等级的原子计数
public static $events = [];
/** @var Atomic[] */
public static $atomics;
public static $req_mapping = [];
public static $config = [];
public static $context = [];
public static $instance = [];
public static $context_class = [];
public static $server_events = [];
static function get($name, $default = null) {
return self::$cache[$name] ?? $default;
}
static function set($name, $value) {
self::$cache[$name] = $value;
}
static function append($name, $value) {
self::$cache[$name][] = $value;
}
static function appendKey($name, $key, $value) {
self::$cache[$name][$key] = $value;
}
static function appendKeyInKey($name, $key, $value) {
self::$cache[$name][$key][] = $value;
}
static function unsetCache($name) {
unset(self::$cache[$name]);
}
static function unsetByValue($name, $vale) {
$key = array_search($vale, self::$cache[$name]);
array_splice(self::$cache[$name], $key, 1);
}
static function isset($name) {
return isset(self::$cache[$name]);
}
static function array_key_exists($name, $key) {
return isset(self::$cache[$name][$key]);
}
static function in_array($name, $val) {
return in_array($val, self::$cache[$name]);
}
static function globals($key) {
return self::$globals->get($key);
}
static function config($config_name) {
return self::$config[$config_name] ?? null;
}
public static function resetCache() {
self::$cache = [];
self::$connect = [];
self::$time_nlp = null;
self::$instance = [];
}
/**
* 初始化atomic计数器
*/
public static function initAtomic() {
foreach (ZMBuf::globals("init_atomics") as $k => $v) {
self::$atomics[$k] = new Atomic($v);
}
}
}

View File

@@ -0,0 +1,227 @@
<?php
use Framework\Console;
use Framework\DataProvider;
use Framework\ZMBuf;
use Swoole\Coroutine\System;
use ZM\Context\ContextInterface;
use ZM\Utils\ZMUtil;
function phar_classloader($p){
$filepath = getClassPath($p);
if($filepath === null) {
Console::debug("F:Warning: get class path wrongs.$p");
return;
}
try {
require_once $filepath;
} catch (Exception $e) {
echo "Error when finding class: " . $p . PHP_EOL;
die;
}
}
function getClassPath($class_name) {
$dir = str_replace("\\", "/", $class_name);
$dir2 = WORKING_DIR . "/src/" . $dir . ".php";
//echo "@@@".$dir2.PHP_EOL;
$dir2 = str_replace("\\", "/", $dir2);
if (file_exists($dir2)) return $dir2;
else {
$dir = DataProvider::getWorkingDir() . "/src/" . $dir . ".php";
//echo "###".$dir.PHP_EOL;
if (file_exists($dir)) return $dir;
else return null;
}
}
/**
* 使用自己定义的万san能分割函数
* @param $msg
* @param bool $ban_comma
* @return array
*/
function explodeMsg($msg, $ban_comma = false) {
$msg = str_replace(" ", "\n", trim($msg));
if (!$ban_comma) {
//$msg = str_replace("", "\n", $msg);
$msg = str_replace("\t", "\n", $msg);
}
$msgs = explode("\n", $msg);
$ls = [];
foreach ($msgs as $k => $v) {
if (trim($v) == "") continue;
$ls[] = trim($v);
}
return $ls;
}
function unicode_decode($str) {
return preg_replace_callback('/\\\\u([0-9a-f]{4})/i', function ($matches) {
return mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UCS-2BE");
}, $str);
}
/**
* 获取模块文件夹下的每个类文件的类名称
* @param $dir
* @param string $indoor_name
* @return array
*/
function getAllClasses($dir, $indoor_name) {
if(!is_dir($dir)) return [];
$list = scandir($dir);
$classes = [];
unset($list[0], $list[1]);
foreach ($list as $v) {
//echo "Finding " . $dir . $v . PHP_EOL;
//echo "At " . $indoor_name . PHP_EOL;
if (is_dir($dir . $v)) $classes = array_merge($classes, getAllClasses($dir . $v . "/", $indoor_name . "\\" . $v));
elseif (mb_substr($v, -4) == ".php") {
$class_name = $indoor_name . "\\" . mb_substr($v, 0, -4);
$classes [] = $class_name;
}
}
return $classes;
}
function matchPattern($pattern, $context) {
if (mb_substr($pattern, 0, 1) == "" && mb_substr($context, 0, 1) == "")
return true;
if ('*' == mb_substr($pattern, 0, 1) && "" != mb_substr($pattern, 1, 1) && "" == mb_substr($context, 0, 1))
return false;
if (mb_substr($pattern, 0, 1) == mb_substr($context, 0, 1))
return matchPattern(mb_substr($pattern, 1), mb_substr($context, 1));
if (mb_substr($pattern, 0, 1) == "*")
return matchPattern(mb_substr($pattern, 1), $context) || matchPattern($pattern, mb_substr($context, 1));
return false;
}
function split_explode($del, $str, $divide_en = false) {
$str = explode($del, $str);
for ($i = 0; $i < mb_strlen($str[0]); $i++) {
if (
is_numeric(mb_substr($str[0], $i, 1)) &&
(
!is_numeric(mb_substr($str[0], $i - 1, 1)) &&
mb_substr($str[0], $i - 1, 1) != ' ' &&
ctype_alpha(mb_substr($str[0], $i - 1, 1)) === false
)
) {
$str[0] = mb_substr($str[0], 0, $i) . " " . mb_substr($str[0], $i);
} elseif (
$divide_en &&
ctype_alnum(mb_substr($str[0], $i, 1)) &&
!ctype_alnum(mb_substr($str[0], $i - 1, 1)) &&
mb_substr($str[0], $i - 1, 1) != ' '
) {
$str[0] = mb_substr($str[0], 0, $i) . ' ' . mb_substr($str[0], $i);
}
}
$str = implode($del, $str);
//echo $str."\n";
$ls = [];
foreach (explode($del, $str) as $k => $v) {
if (trim($v) == "") continue;
$ls[] = $v;
}
//var_dump($ls);
return $ls == [] ? [""] : $ls;
}
function matchArgs($pattern, $context) {
$result = [];
if (matchPattern($pattern, $context)) {
if (mb_strpos($pattern, "*") === false) return [];
$exp = explode("*", $pattern);
$i = 0;
foreach ($exp as $k => $v) {
//echo "[MATCH$k] " . $v . PHP_EOL;
if ($v == "" && $k == 0) continue;
elseif ($v == "" && $k == count($exp) - 1) {
$context = $context . "^EOL";
$v = "^EOL";
}
$cur_var = "";
//echo mb_substr($context, $i) . "|" . $v . PHP_EOL;
$ori = $i;
while (($a = mb_substr($context, $i, mb_strlen($v))) != $v && $a != "") {
$cur_var .= mb_substr($context, $i, 1);
++$i;
}
if ($i != $ori || $k == 1 || $k == count($exp) - 1) {
//echo $cur_var . PHP_EOL;
$result[] = $cur_var;
}
$i += mb_strlen($v);
}
return $result;
} else return false;
}
function set_coroutine_params($array) {
$cid = Co::getCid();
if ($cid == -1) die("Cannot set coroutine params at none coroutine mode.");
if (isset(ZMBuf::$context[$cid])) ZMBuf::$context[$cid] = array_merge(ZMBuf::$context[$cid], $array);
else ZMBuf::$context[$cid] = $array;
foreach (ZMBuf::$context as $c => $v) {
if (!Co::exists($c)) unset(ZMBuf::$context[$c], ZMBuf::$context_class[$c]);
}
}
/**
* @return ContextInterface|null
*/
function context() {
return ctx();
}
/**
* @return ContextInterface|null
*/
function ctx() {
$cid = Co::getCid();
$c_class = ZMBuf::globals("context_class");
if (isset(ZMBuf::$context[$cid])) {
return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid));
} else {
Console::debug("未找到当前协程的上下文($cid),正在找父进程的上下文");
while (($pcid = Co::getPcid($cid)) !== -1) {
$cid = $pcid;
if (isset(ZMBuf::$context[$cid])) return ZMBuf::$context_class[$cid] ?? (ZMBuf::$context_class[$cid] = new $c_class($cid));
}
return null;
}
}
function debug($msg) { Console::debug($msg); }
function zm_sleep($s = 1) { Co::sleep($s); }
function zm_exec($cmd): array { return System::exec($cmd); }
function zm_cid() { return Co::getCid(); }
function zm_yield() { Co::yield(); }
function zm_resume(int $cid) { Co::resume($cid); }
function zm_timer_after($ms, callable $callable) {
go(function () use ($ms, $callable) {
ZMUtil::checkWait();
Swoole\Timer::after($ms, $callable);
});
}
function zm_timer_tick($ms, callable $callable) {
go(function () use ($ms, $callable) {
ZMUtil::checkWait();
Console::debug("Adding extra timer tick of " . $ms . " ms");
Swoole\Timer::tick($ms, $callable);
});
}

View File

@@ -0,0 +1,33 @@
<?php
use Swoole\Coroutine\Http\Client;
Co\run(function (){
hello:
global $terminal_id, $port;
$client = new Client("127.0.0.1", $port);
$client->set(['websocket_mask' => true]);
$client->setHeaders(["x-terminal-id" => $terminal_id, 'x-pid' => posix_getppid()]);
$ret = $client->upgrade("/?type=terminal");
if ($ret) {
while (true) {
$line = fgets(STDIN);
if ($line !== false) {
$r = $client->push(trim($line));
if (trim($line) == "reload" || trim($line) == "r" || trim($line) == "stop") {
break;
}
if($r === false) {
echo "Unable to connect framework terminal, connection closed. Trying to reconnect after 5s.\n";
sleep(5);
goto hello;
}
} else {
break;
}
}
} else {
echo "Unable to connect framework terminal. port: $port\n";
}
});

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
class_alias(\ZM\Annotation\Framework\BindEvent::class, 'BindEvent');
class_alias(\ZM\Annotation\Framework\Init::class, 'Init');
class_alias(\ZM\Annotation\Framework\Setup::class, 'Setup');
class_alias(\ZM\Annotation\Http\Controller::class, 'Controller');
class_alias(\ZM\Annotation\Http\Route::class, 'Route');
class_alias(\ZM\Annotation\Middleware\Middleware::class, 'Middleware');
class_alias(\ZM\Annotation\OneBot\BotCommand::class, 'BotCommand');
class_alias(\ZM\Annotation\OneBot\BotEvent::class, 'BotEvent');
class_alias(\ZM\Annotation\OneBot\CommandArgument::class, 'CommandArgument');
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');
class_alias(\ZM\Store\KV\Redis\KVRedis::class, 'KVRedis');
// 下面是 OneBot 相关类的全局别称
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketOpenEvent::class, 'WebSocketOpenEvent');
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketCloseEvent::class, 'WebSocketCloseEvent');
class_alias(\OneBot\Driver\Event\WebSocket\WebSocketMessageEvent::class, 'WebSocketMessageEvent');
class_alias(\OneBot\Driver\Event\Http\HttpRequestEvent::class, 'HttpRequestEvent');

View File

@@ -1,93 +0,0 @@
<?php
declare(strict_types=1);
/*
此文件用于定义一些常量,是框架运行的前提常量,例如定义全局版本、全局方法等。
*/
use ZM\Framework;
/** 全局版本ID */
const ZM_VERSION_ID = Framework::VERSION_ID;
/** 全局版本名称 */
const ZM_VERSION = Framework::VERSION;
/** 机器人用的,用于判断二元语意 */
const TRUE_LIST = ['yes', 'ok', 'y', 'true', 'on', '是', '对', true];
const FALSE_LIST = ['no', 'n', 'false', 'off', '否', '错', false];
/** 定义多进程的全局变量 */
const ZM_PROCESS_MASTER = ONEBOT_PROCESS_MASTER;
const ZM_PROCESS_MANAGER = ONEBOT_PROCESS_MANAGER;
const ZM_PROCESS_WORKER = ONEBOT_PROCESS_WORKER;
const ZM_PROCESS_USER = ONEBOT_PROCESS_USER;
const ZM_PROCESS_TASKWORKER = ONEBOT_PROCESS_TASKWORKER;
/** 定义一些内部引用的错误ID */
const ZM_ERR_NONE = 0; // 正常
const ZM_ERR_METHOD_NOT_FOUND = 1; // 找不到方法
const ZM_ERR_ROUTE_NOT_FOUND = 2; // 找不到路由
const ZM_ERR_ROUTE_METHOD_NOT_ALLOWED = 3; // 路由方法不允许
/** 定义 BotContext 下 reply 回复的模式 */
const ZM_REPLY_NONE = 0; // 默认回复,不带任何东西
const ZM_REPLY_MENTION = 1; // 回复时 @ 该用户
const ZM_REPLY_QUOTE = 2; // 回复时引用该消息
const LOAD_MODE_VENDOR = 0; // 从 vendor 加载
const LOAD_MODE_SRC = 1; // 从 src 加载
/* 定义工作目录 */
define('WORKING_DIR', getcwd());
/* 定义源码根目录,如果是 Phar 打包框架运行的话,就是 Phar 文件本身 */
define('SOURCE_ROOT_DIR', Phar::running() !== '' ? Phar::running() : WORKING_DIR);
if (DIRECTORY_SEPARATOR === '\\') {
define('TMP_DIR', 'C:\\Windows\\Temp');
} elseif (!empty(getenv('TMPDIR'))) {
define('TMP_DIR', getenv('TMPDIR'));
} elseif (is_writable('/tmp')) {
define('TMP_DIR', '/tmp');
} else {
define('TMP_DIR', getcwd() . '/.zm-tmp');
}
/* 定义启动模式,这里指的是框架本身的源码目录是通过 composer 加入 vendor 加载的还是直接放到 src 目录加载的,前者为 1后者为 0 */
define('LOAD_MODE', is_dir(zm_dir(SOURCE_ROOT_DIR . '/src/ZM')) ? LOAD_MODE_VENDOR : LOAD_MODE_SRC);
/* 定义框架本身所处的根目录,此处如果 LOAD_MODE 为 1 的话,框架自身的根目录在 vendor/zhamao/framework 子目录下 */
if (Phar::running() !== '') {
define('FRAMEWORK_ROOT_DIR', zm_dir(LOAD_MODE == 1 ? (SOURCE_ROOT_DIR . '/vendor/zhamao/framework') : SOURCE_ROOT_DIR));
} else {
define('FRAMEWORK_ROOT_DIR', realpath(zm_dir(__DIR__ . '/../../')));
}
define('ZM_INIT_TIME', microtime(true));
/* 定义用于存放框架运行状态的目录Windows 可用) */
define('ZM_STATE_DIR', TMP_DIR . '/.zm_' . sha1(ZM_INIT_TIME . FRAMEWORK_ROOT_DIR));
/* 对 global.php 在 Windows 下的兼容性考虑,因为 Windows 或者无 Swoole 环境时候无法运行 */
!defined('SWOOLE_BASE') && define('SWOOLE_BASE', 1) && define('SWOOLE_PROCESS', 2);
!defined('SWOOLE_HOOK_ALL') && (
define('SWOOLE_HOOK_TCP', 2)
&& define('SWOOLE_HOOK_UDP', 4)
&& define('SWOOLE_HOOK_UNIX', 8)
&& define('SWOOLE_HOOK_UDG', 16)
&& define('SWOOLE_HOOK_SSL', 32)
&& define('SWOOLE_HOOK_TLS', 64)
&& define('SWOOLE_HOOK_STREAM_FUNCTION', 128)
&& define('SWOOLE_HOOK_STREAM_SELECT', 128)
&& define('SWOOLE_HOOK_FILE', 256)
&& define('SWOOLE_HOOK_STDIO', 32768)
&& define('SWOOLE_HOOK_SLEEP', 512)
&& define('SWOOLE_HOOK_PROC', 1024)
&& define('SWOOLE_HOOK_CURL', 2048)
&& define('SWOOLE_HOOK_NATIVE_CURL', 4096)
&& define('SWOOLE_HOOK_BLOCKING_FUNCTION', 8192)
&& define('SWOOLE_HOOK_SOCKETS', 16384)
&& define('SWOOLE_HOOK_ALL', 2_147_481_599)
);

View File

@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
use ZM\Utils\ZMUtil;
/* 定义炸毛框架初始启动时间 */
if (!defined('ZM_START_TIME')) {
define('ZM_START_TIME', microtime(true));
}
/* 定义使用炸毛框架应用的版本 */
if (!defined('APP_VERSION')) {
define('APP_VERSION', LOAD_MODE == 1 ? (ZMUtil::getComposerMetadata()['version'] ?? ZM_VERSION) : ZM_VERSION);
}

View File

@@ -1,270 +0,0 @@
<?php
declare(strict_types=1);
use OneBot\Driver\Coroutine\Adaptive;
use OneBot\Driver\Coroutine\CoroutineInterface;
use OneBot\Driver\Process\ExecutionResult;
use OneBot\V12\Object\MessageSegment;
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;
use ZM\Store\KV\Redis\RedisWrapper;
// 防止重复引用引发报错
if (function_exists('zm_internal_errcode')) {
return;
}
/**
* 根据具体操作系统替换目录分隔符
*
* @param string $dir 目录
*/
function zm_dir(string $dir): string
{
if (str_starts_with($dir, 'phar://')) {
return $dir;
}
return str_replace('/', DIRECTORY_SEPARATOR, $dir);
}
/**
* 执行shell指令
*
* @param string $cmd 命令行
*/
function zm_exec(string $cmd): ExecutionResult
{
return Adaptive::exec($cmd);
}
/**
* sleep 指定时间单位为秒最小单位为1毫秒即0.001
*/
function zm_sleep(float|int $time)
{
Adaptive::sleep($time);
}
/**
* 获取协程接口
*/
function coroutine(): ?CoroutineInterface
{
return Adaptive::getCoroutine();
}
/**
* 获取内部错误码
*/
function zm_internal_errcode(int|string $code): string
{
return "[ErrCode:{$code}] ";
}
/**
* 返回当前炸毛实例的 ID
*/
function zm_instance_id(): string
{
if (defined('ZM_INSTANCE_ID')) {
return ZM_INSTANCE_ID;
}
if (!defined('ZM_START_TIME')) {
define('ZM_START_TIME', microtime(true));
}
$instance_id = dechex(crc32(strval(ZM_START_TIME)));
define('ZM_INSTANCE_ID', $instance_id);
return ZM_INSTANCE_ID;
}
/**
* 助手方法,返回一个 Logger 实例
*/
function logger(): LoggerInterface
{
global $ob_logger;
if ($ob_logger === null) {
return new ConsoleLogger();
}
return $ob_logger;
}
/**
* 判断传入的数组是否为关联数组
*/
function is_assoc_array(array $array): bool
{
return !empty($array) && array_keys($array) !== range(0, count($array) - 1);
}
/**
* 格式匹配
*/
function match_pattern(string $pattern, string $subject): bool
{
$pattern = str_replace(['\*', '\\\\.*'], ['.*', '\*'], preg_quote($pattern, '/'));
$pattern = '/^' . $pattern . '$/i';
return preg_match($pattern, $subject) === 1;
}
/**
* 匹配参数
*
* @return array|false 成功时返回匹配到的参数数组失败时返回false
*/
function match_args(string $pattern, string $subject)
{
$result = [];
if (match_pattern($pattern, $subject)) {
if (mb_strpos($pattern, '*') === false) {
return [];
}
$exp = explode('*', $pattern);
$i = 0;
foreach ($exp as $k => $v) {
if (empty($v) && $k === 0) {
continue;
}
if (empty($v) && $k === count($exp) - 1) {
$subject .= '^EOL';
$v = '^EOL';
}
$cur_var = '';
$ori = $i;
while (($a = mb_substr($subject, $i, mb_strlen($v))) !== $v && !empty($a)) {
$cur_var .= mb_substr($subject, $i, 1);
++$i;
}
if ($i !== $ori || $k === 1 || $k === count($exp) - 1) {
$result[] = $cur_var;
}
$i += mb_strlen($v);
}
return $result;
}
return false;
}
/**
* 构建消息段的助手函数
*
* @param string $type 类型
* @param array $data 字段
*/
function segment(string $type, array $data = []): MessageSegment
{
return new MessageSegment($type, $data);
}
/**
* 中间件操作类的助手函数
*/
function middleware(): MiddlewareHandler
{
return MiddlewareHandler::getInstance();
}
// ////////////////// 容器部分 //////////////////////
/**
* 获取容器实例
*/
function container(): DI\Container
{
return ContainerHolder::getEventContainer();
}
/**
* 解析类实例(使用容器)
*
* 这是 {@see container()}->make($abstract, $parameters) 的别名
*
* @template T
* @param class-string<T> $abstract
* @return Closure|mixed|T
*/
function resolve(string $abstract, array $parameters = [])
{
/* @noinspection PhpUnhandledExceptionInspection */
return container()->make($abstract, $parameters);
}
/**
* 获取 MySQL 调用的类
*
* @throws DBException
*/
function db(string $name = '')
{
return new DBWrapper($name);
}
/**
* 获取构建 MySQL 的类
*
* @throws DBException
*/
function sql_builder(string $name = ''): DBQueryBuilder
{
return (new DBWrapper($name))->createQueryBuilder();
}
/**
* 获取 Redis 操作类
*
* @param string $name 使用的 Redis 连接名称
*/
function redis(string $name = 'default'): RedisWrapper
{
return new RedisWrapper($name);
}
/**
* 获取 / 设置配置项
*
* 传入键名和(或)默认值,获取配置项
* 传入数组,设置配置项
* 不传参数,返回配置容器
*
* @param null|array|string $key 键名
* @param null|mixed $default 默认值
* @return mixed|void|ZMConfig
*/
function config(array|string $key = null, mixed $default = null)
{
$config = ZMConfig::getInstance();
if (is_null($key)) {
return $config;
}
if (is_array($key)) {
$config->set($key);
return;
}
return $config->get($key, $default);
}
function bot(): ZM\Context\BotContext
{
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);
}

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Framework\Setup;
use ZM\Utils\ZMUtil;
function _zm_setup_loader()
{
try {
global $_tmp_setup_list;
$_tmp_setup_list = [];
$parser = new AnnotationParser(false);
$composer = ZMUtil::getComposerMetadata();
// 合并 dev 和 非 dev 的 psr-4 加载目录
$merge_psr4 = array_merge($composer['autoload']['psr-4'] ?? [], $composer['autoload-dev']['psr-4'] ?? []);
// 排除 composer.json 中指定需要排除的目录
$excludes = $composer['extra']['zm']['exclude-annotation-path'] ?? [];
foreach ($merge_psr4 as $k => $v) {
// 如果在排除表就排除,否则就解析注解
if (is_dir(SOURCE_ROOT_DIR . '/' . $v) && !in_array($v, $excludes)) {
// 添加解析路径对应Base命名空间也贴出来
$parser->addPsr4Path(SOURCE_ROOT_DIR . '/' . $v . '/', trim($k, '\\'));
}
}
$parser->addSpecialParser(Setup::class, function (Setup $setup) {
global $_tmp_setup_list;
$_tmp_setup_list[] = [
'class' => $setup->class,
'method' => $setup->method,
];
return true;
});
// TODO: 然后加载插件目录下的插件
// 解析所有注册路径的文件,获取注解
$parser->parse();
return json_encode(['setup' => $_tmp_setup_list]);
} catch (Throwable $e) {
$stderr = fopen('php://stderr', 'w');
fwrite($stderr, zm_internal_errcode('E00031') . $e->getMessage() . ' in ' . $e->getFile() . ' at line ' . $e->getLine() . PHP_EOL);
fclose($stderr);
exit(1);
}
}
// 在*nix等支持多进程环境的情况可直接运行此文件那么就执行
if (debug_backtrace() === []) {
require((!is_dir(__DIR__ . '/../../vendor')) ? getcwd() : (__DIR__ . '/../..')) . '/vendor/autoload.php';
echo _zm_setup_loader();
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Module\Example;
use Framework\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\SwooleEventAt;
use ZM\Connection\CQConnection;
use ZM\Utils\ZMUtil;
/**
* Class Hello
* @package Module\Example
* @since 1.0
*/
class Hello
{
/**
* 在机器人连接后向终端输出信息
* @SwooleEventAt("open",rule="connectType:qq")
* @param $conn
*/
public function onConnect(CQConnection $conn) {
Console::info("机器人 " . $conn->getQQ() . " 已连接!");
}
/**
* 在机器人连接后向终端输出信息
* @SwooleEventAt("close",rule="connectType:qq")
*/
public function onDisconnect() {
$conn = ctx()->getConnection();
Console::info("机器人 " . $conn->getQQ() . " 已断开连接!");
}
/**
* 向机器人发送"你好",即可回复这句话
* @CQCommand(match="你好",alias={"你好啊","你是谁"})
*/
public function hello() {
return "你好啊,我是由炸毛框架构建的机器人!";
}
/**
* @CQCommand(".reload")
*/
public function reload() {
context()->reply("reloading...");
ZMUtil::reload();
}
/**
* @CQCommand("随机数")
* @CQCommand(regexMatch="*从*到*的随机数")
* @param $arg
*/
public function randNum($arg) {
// 获取第一个数字类型的参数
$num1 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第一个数字");
// 获取第二个数字类型的参数
$num2 = context()->getArgs($arg, ZM_MATCH_NUMBER, "请输入第二个数字");
$a = min(intval($num1), intval($num2));
$b = max(intval($num1), intval($num2));
// 回复用户结果
context()->reply("随机数是:".mt_rand($a, $b));
}
/**
* 中间件测试的一个示例函数
* @RequestMapping("/httpTimer")
* @Middleware("timer")
*/
public function timer() {
return "This page is used as testing TimerMiddleware! Do not use it in production.";
}
/**
* 默认示例页面
* @RequestMapping("/index")
* @RequestMapping("/")
*/
public function index() {
return "Hello Zhamao!";
}
/**
* 框架会默认关闭未知的WebSocket链接因为这个绑定的事件你可以根据你自己的需求进行修改
* @SwooleEventAt(type="open",rule="connectType:unknown")
*/
public function closeUnknownConn() {
Console::info("Unknown connection , I will close it.");
context()->getConnection()->close();
}
}

View File

@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace Module\Example;
use OneBot\Driver\Coroutine\Adaptive;
use OneBot\Driver\Event\WebSocket\WebSocketMessageEvent;
use ZM\Annotation\Framework\Cron;
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
{
#[Route('/route', request_method: ['GET'])]
#[Middleware(TimerMiddleware::class)]
public function route()
{
return 'Hello ZhamaoThis is the first 3.0 page';
}
#[BotEvent()]
public function onOBEvent(\OneBotEvent $event, WebSocketMessageEvent $messageEvent): void
{
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());
}
#[Cron('* * * * *', no_overlap: true)]
public function logTime(): void
{
$time = date('Y-m-d H:i:s');
logger()->info('我看到时间了,让我写下来');
Adaptive::sleep(5);
logger()->info('写好啦,时间是' . $time);
}
#[Cron('* * * * *')]
public function logTime2(): void
{
logger()->info('我不需要等,但也不给你看时间');
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Module\Middleware;
use Framework\Console;
use ZM\Annotation\Http\After;
use ZM\Annotation\Http\Before;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Http\MiddlewareInterface;
/**
* Class AuthMiddleware
* 示例中间件:用于统计路由函数运行时间用的
* @package Module\Middleware
* @MiddlewareClass()
*/
class TimerMiddleware implements MiddlewareInterface
{
private $starttime;
/**
* @Before()
* @return bool
*/
public function onBefore() {
$this->starttime = microtime(true);
return true;
}
/**
* @After()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
}
public function getName() { return "timer"; }
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Scheduler;
use Swoole\Coroutine\Http\Client;
use Swoole\WebSocket\Frame;
class MessageEvent
{
/**
* @var Frame
*/
private $frame;
/**
* @var Client
*/
private $client;
public function __construct(Client $client, Frame $frame) {
$this->client = $client;
$this->frame = $frame;
}
public function onActivate() {
//TODO: 写Scheduler计时器内的处理逻辑
}
}

140
src/Scheduler/Scheduler.php Normal file
View File

@@ -0,0 +1,140 @@
<?php
namespace Scheduler;
use Exception;
use Framework\Console;
use Framework\GlobalConfig;
use Swoole\Coroutine;
use Swoole\Coroutine\Http\Client;
use Swoole\Process;
use Swoole\WebSocket\Frame;
class Scheduler
{
const PROCESS = 1;
const REMOTE = 2;
/**
* @var Process
*/
private $process = null;
/**
* @var int
*/
private $m_pid;
private $pid;
/**
* @var Scheduler
*/
private static $instance;
/**
* @var GlobalConfig
*/
private $settings;
/**
* @var Client
*/
private $client;
public function __construct($method = self::PROCESS, $option = []) {
if (self::$instance !== null) die("Cannot run two scheduler in on process!");
self::$instance = $this;
if ($method == self::PROCESS) $this->initProcess();
elseif ($method == self::REMOTE) $this->initRemote();
}
private function initProcess() { //TODO: 完成Process模式的代码
$m_pid = posix_getpid();
$this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true);
$this->pid = $this->process->start();
while (1) {
$ret = Process::wait();
if ($ret) {
$this->process = new Process(function (Process $worker) use ($m_pid) { self::onWork($worker, $m_pid); }, false, 2, true);
$this->pid = $this->process->start();
echo "Reboot done.\n";
}
}
}
private function initRemote() {
define('WORKING_DIR', __DIR__ . '../..');
$this->requireGlobalFunctions();
$this->registerAutoloader('classLoader');
$this->settings = new GlobalConfig();
if (!$this->settings->success) die("Failed to load global config. Please check config/global.php file");
$this->defineProperties();
//start swoole Framework
$this->selfCheck();
try {
$host = $this->settings->get("scheduler")["host"];
$port = $this->settings->get("scheduler")["port"];
$token = $this->settings->get("scheduler")["token"];
$this->client = new Client($host, $port);
$path = "/" . ($token != "" ? ("?token=" . urlencode($token)) : "");
while (true) {
if ($this->client->upgrade($path)) {
while (true) {
$recv = $this->client->recv();
if ($recv instanceof Frame) {
(new MessageEvent($this->client, $recv))->onActivate();
} else {
break;
}
}
} else {
Console::warning("无法连接Framework将在5秒后重连...");
Coroutine::sleep(5);
}
}
} catch (Exception $e) {
Console::error($e);
}
}
private function requireGlobalFunctions() {
/** @noinspection PhpIncludeInspection */
require WORKING_DIR . '/src/Framework/global_functions.php';
}
private function registerAutoloader(string $string) {
if (!spl_autoload_register($string)) die("Failed to register autoloader named \"$string\" !");
}
private function defineProperties() {
define("ZM_START_TIME", microtime(true));
define("ZM_DATA", $this->settings->get("zm_data"));
//define("CONFIG_DIR", $this->settings->get("config_dir"));
define("CRASH_DIR", $this->settings->get("crash_dir"));
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (!function_exists("mb_substr")) die("Can not find mbstring extension.\n");
if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\n");
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
return true;
}
private static function onWork(Process $worker, $m_pid) {
swoole_set_process_name('php-scheduler');
for ($j = 0; $j < 16000; $j++) {
self::checkMpid($worker, $m_pid);
echo "msg: {$j}\n";
sleep(1);
}
}
private static function checkMpid(Process $worker, $m_pid) {
if (!Process::kill($m_pid, 0)) {
$worker->exit(); //主进程死了我也死
// 这句提示,实际是看不到的.需要写到日志中
echo "Master process exited, I [{$worker['pid']}] also quit\n";
}
}
}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace {namespace};
class {class}
{
#[\BotCommand(match: '测试{name}')]
public function firstBotCommand(\BotContext $ctx): void
{
$ctx->reply('这是{name}插件的第一个命令!');
}
}

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
$plugin = new ZMPlugin(__DIR__);
/*
* 发送 "测试{name}",回复 "这是{name}插件的第一个命令!"
*/
$cmd1 = BotCommand::make('{name}', match: '测试{name}')->on(fn () => '这是{name}插件的第一个命令!');
$plugin->addBotCommand($cmd1);
return $plugin;

Some files were not shown because too many files have changed in this diff Show More