Compare commits

...

143 Commits
2.4.3 ... 2.7.0

Author SHA1 Message Date
crazywhalecc
6d90be164a update docs 2022-03-20 22:16:34 +08:00
crazywhalecc
4da6f5859a update to 2.7.0 release (build 447) 2022-03-20 22:12:58 +08:00
crazywhalecc
15d4ea710a add --no-state-check option (build 446) 2022-03-20 21:04:07 +08:00
crazywhalecc
f0541c1f32 Merge remote-tracking branch 'origin/master' 2022-03-20 19:05:54 +08:00
crazywhalecc
2b8cab1824 add AnnotationReader ignore name config (build 445, 2.7.0-beta5) 2022-03-20 19:05:13 +08:00
Jerry Ma
61c7972915 Update README.md 2022-03-20 17:09:24 +08:00
crazywhalecc
44a0eec74c change to integration-test 2022-03-20 17:00:37 +08:00
crazywhalecc
e57753e44b change to integration-test 2022-03-20 16:58:53 +08:00
crazywhalecc
74e91a2950 fix unpack autoload not working, change exclude_annotate to another name (build 444) 2022-03-20 16:51:48 +08:00
crazywhalecc
7ce3ef41df fix comment spacing problem (build 443) 2022-03-20 16:23:07 +08:00
crazywhalecc
444a77933a change autoload to hotload for phar hotload mode (build 442) 2022-03-20 16:20:14 +08:00
crazywhalecc
78f78c607d add packer namespace adjust (build 441) 2022-03-20 16:18:33 +08:00
crazywhalecc
69155002dc add site to gitignore 2022-03-20 16:13:51 +08:00
crazywhalecc
475d14fab7 update composer.json 2022-03-20 16:13:33 +08:00
crazywhalecc
f4d7e63358 update php cs fixer 2022-03-20 15:48:14 +08:00
crazywhalecc
f222d2b45b update composer requirement version 2022-03-20 15:43:31 +08:00
Jerry Ma
d0155fe1da Create code-style-analysis.yml 2022-03-20 15:35:10 +08:00
Jerry Ma
4737d0b507 Update and rename main.yml to mkdocs-deploy.yml 2022-03-20 15:23:05 +08:00
Jerry Ma
09bd0197bb Merge pull request #64 from sunxyw/patch-1
docs: add weather bot example
2022-03-20 15:20:59 +08:00
crazywhalecc
f0f120bd32 add Macroable (build 440) 2022-03-20 01:54:14 +08:00
crazywhalecc
c897da29c6 add PHP8 Attribute compatibility (build 439, 2.7.0-beta4) 2022-03-20 01:53:36 +08:00
sunxyw
e347e254e8 update docs index 2022-03-20 00:11:45 +08:00
crazywhalecc
12363aebf0 Merge remote-tracking branch 'origin/master' 2022-03-19 23:23:54 +08:00
crazywhalecc
ff0b925313 update docs 2022-03-19 23:23:14 +08:00
sunxyw
a6b4bd9b80 update weather bot example 2022-03-19 21:35:48 +08:00
sunxyw
485fa5476c add weather bot example 2022-03-19 21:25:10 +08:00
Jerry Ma
689076d97c Update mkdocs.yml 2022-03-19 20:33:13 +08:00
Jerry Ma
cca6102e91 Update README.md 2022-03-18 16:57:27 +08:00
Jerry Ma
095855162b Update mkdocs.yml 2022-03-18 16:48:30 +08:00
Jerry Ma
326f934013 Update mkdocs.yml 2022-03-18 16:44:04 +08:00
Jerry Ma
35b0c258fe Merge pull request #63 from sunxyw/patch-2
docs: add qingyunke chatbot integration example
2022-03-18 16:01:15 +08:00
sunxyw
6650846b15 update integrate-qingyunke-chatbot.md 2022-03-18 15:19:19 +08:00
sunxyw
a33d320f4c update integrate-qingyunke-chatbot.md 2022-03-18 02:54:23 +08:00
sunxyw
0bcfea6aa4 add qingyunke chatbot integration example 2022-03-18 02:30:45 +08:00
crazywhalecc
db6e63e91c fix Response class null error (build 438) 2022-03-17 20:48:09 +08:00
crazywhalecc
a7f84fb53a Merge remote-tracking branch 'origin/master' 2022-03-17 19:48:38 +08:00
crazywhalecc
ce7f2b1765 change ctx return force to ContextInterface (build 437) 2022-03-17 19:42:59 +08:00
Jerry Ma
abbfb59eff Merge pull request #62 from sunxyw/patch-2
docs: add missing module version example
2022-03-17 18:19:02 +08:00
sunxyw
b57fef16f9 add missing module version example 2022-03-17 01:53:06 +08:00
crazywhalecc
1706afbcd0 add cs fixer and PHPStan and activate it (build 436) 2022-03-15 18:05:33 +08:00
crazywhalecc
d01bd69aa5 update to 2.7.0-beta1 (build 435) 2022-03-13 22:50:32 +08:00
crazywhalecc
e6b9ae3ee1 add --watch function for no-installed-inotify users 2022-03-13 22:50:01 +08:00
crazywhalecc
73b6b8045d enhancement for process state 2022-03-13 22:47:11 +08:00
crazywhalecc
3c87abc6e8 enhancement for process state 2022-03-13 22:46:22 +08:00
crazywhalecc
e95925c129 add compatibility for PHP 8.1 2022-03-13 22:16:02 +08:00
crazywhalecc
82c44d6c40 enhancement for process state 2022-03-13 22:15:27 +08:00
crazywhalecc
e0a268e05e add autoload-dev auto scanner 2022-03-13 22:11:30 +08:00
crazywhalecc
487892e1d9 add force kill framework command --force 2022-03-13 22:05:53 +08:00
crazywhalecc
7ab4e88359 add KILLER PROMPT function 2022-03-13 22:05:23 +08:00
crazywhalecc
4702b6ee75 separate ProcessManager and WorkerManager 2022-03-13 22:04:52 +08:00
crazywhalecc
20cd3aa66d update docs 2022-03-13 22:03:52 +08:00
crazywhalecc
391114bdef update docs 2022-03-13 21:57:41 +08:00
crazywhalecc
ffe1052ecc add gitignore items 2022-03-13 21:54:43 +08:00
crazywhalecc
b4159152a7 add watcher 2022-01-08 20:19:10 +08:00
crazywhalecc
8fc6e4b0f7 update docs 2022-01-08 16:26:47 +08:00
crazywhalecc
c8938b7a4b update docs 2022-01-08 16:23:10 +08:00
crazywhalecc
34db1626a5 update to 2.6.6 (build 434) 2022-01-08 16:19:43 +08:00
crazywhalecc
7f0c97c5b9 update to 2.6.5 (build 433) 2021-12-28 22:40:40 +08:00
crazywhalecc
74050c46e7 update to 2.6.4 (build 432) 2021-12-25 19:21:41 +08:00
3ed1cb665a update to build 431 2021-12-22 14:39:46 +08:00
crazywhalecc
a6f33ba69d update to build 430 2021-12-08 21:29:38 +08:00
Jerry Ma
176b690417 Update v2.md 2021-12-07 13:28:21 +08:00
Jerry Ma
e22b1b90ec Update build-update.md 2021-12-07 13:27:33 +08:00
Jerry Ma
a3c560790c Merge pull request #55 from zhamao-robot/fix/cqafter
update to build 429
2021-12-07 13:25:55 +08:00
570e2108dd - 新增配置项 onebot.message_command_policy
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
2021-12-07 13:21:29 +08:00
Jerry Ma
59c0d95e5d Create 2_Feature_request.yaml 2021-12-07 12:34:18 +08:00
Jerry Ma
9ceaecdc02 Update README.md 2021-12-07 12:17:47 +08:00
Jerry Ma
19d50898ef Create 1_Bug_report.yaml 2021-12-07 12:15:49 +08:00
Jerry Ma
5b62ca62ae Update README.md 2021-11-17 00:28:54 +08:00
fb528d30ce update docs 2021-11-16 16:51:05 +08:00
5f2d5ed334 update to build 428 2021-11-16 16:49:32 +08:00
c20e459900 update docs 2021-11-16 15:44:34 +08:00
09220825cf update to build 427 2021-11-16 15:41:01 +08:00
Jerry Ma
3d4db23d27 Update v2.md 2021-11-10 14:10:08 +08:00
Jerry Ma
4496b67dcc Update build-update.md 2021-11-10 14:09:18 +08:00
Jerry Ma
2a13298384 update to 2.5.8 (build 426) 2021-11-10 14:07:21 +08:00
Jerry Ma
d6ec404d76 Merge pull request #52 from YuFengZe/master
Update CQ.php
2021-11-10 14:06:13 +08:00
YuFengZe
3235fd4dc1 Update CQ.php 2021-11-08 22:05:41 +08:00
crazywhalecc
293740fee2 update to 2.5.7 (build 425) 2021-11-03 23:26:43 +08:00
crazywhalecc
d3c420ec84 update to build 424 (2.6.0-alpha1) 2021-11-02 16:01:24 +08:00
crazywhalecc
85ef09d43c Merge remote-tracking branch 'origin/master' 2021-11-01 00:29:11 +08:00
Jerry Ma
e4561d69c4 Merge pull request #45 from YuFengZe/master
Bug Fixed
2021-10-31 22:50:49 +08:00
Jerry Ma
6b4d206099 Update Hello.php 2021-10-31 22:50:12 +08:00
YuFengZe
50843edf6a Bug Fixed
解决发送“我是谁”却返回机器人信息的奇怪问题。
2021-10-31 22:01:14 +08:00
crazywhalecc
3a1686f8da update docs 2021-10-18 22:24:28 +08:00
crazywhalecc
66dd91bb97 update to 2.5.6 (build 423) 2021-10-17 23:55:02 +08:00
crazywhalecc
e020e5d593 update docs 2021-10-17 17:00:33 +08:00
crazywhalecc
3d62663281 update docs 2021-10-17 16:56:52 +08:00
crazywhalecc
8d9485c02e update docs 2021-10-17 15:37:55 +08:00
Jerry Ma
9fb45dd683 Update README.md 2021-10-15 09:38:07 +08:00
Jerry Ma
8a4924dba9 Update light-cache.md 2021-10-08 11:41:40 +08:00
crazywhalecc
beaf7be606 update to version 2.5.5 (buid 422) 2021-10-06 18:01:40 +08:00
Jerry Ma
7dbd21bdf4 Merge pull request #44 from furleywolf/patch-1
更新message-util的文档
2021-09-15 12:46:48 +08:00
furleywolf
9ce3056203 更新message-util的文档
发现漏掉了一个方法,还是补上去吧
2021-09-13 17:35:39 +08:00
crazywhalecc
880b4e847c update to version 2.5.4 (buid 421) 2021-09-11 16:27:16 +08:00
crazywhalecc
71e83d5bc8 update docs 2021-09-11 12:06:22 +08:00
crazywhalecc
432fd92cca update docs 2021-09-11 12:00:48 +08:00
crazywhalecc
907a9cea25 update to build 420 2021-09-11 11:59:02 +08:00
07391810ff update to build 419 2021-09-11 11:07:23 +08:00
5063421364 update docs 2021-09-10 11:28:56 +08:00
2b4d308783 update to build 418 2021-09-10 11:24:32 +08:00
e2f49968b3 Merge remote-tracking branch 'origin/master' 2021-09-01 14:14:13 +08:00
229778ebf9 update to build 417 2021-09-01 14:14:00 +08:00
Jerry Ma
d2c0972c93 Add actions badge [skip ci] 2021-08-31 13:40:29 +08:00
Jerry Ma
d300b6e518 Update main.yml 2021-08-31 13:39:11 +08:00
Jerry Ma
56cb7b2223 Update main.yml 2021-08-30 16:42:47 +08:00
Jerry Ma
2fc42d5d60 Update README.md 2021-08-30 16:40:53 +08:00
Jerry Ma
999e90f709 Update index.md 2021-08-30 16:17:13 +08:00
Jerry Ma
45c6cd7d2a Update main.yml 2021-08-30 16:12:35 +08:00
Jerry Ma
5aa0858021 Update main.yml 2021-08-30 16:05:15 +08:00
Jerry Ma
e981da3932 Update main.yml 2021-08-30 16:00:36 +08:00
Jerry Ma
2685be7306 Create main.yml 2021-08-30 11:27:51 +08:00
Jerry Ma
a13c4628f5 Update README.md 2021-07-15 11:37:35 +08:00
Jerry Ma
b36417454c Update README.md 2021-07-10 15:55:15 +08:00
crazywhalecc
a6f5952dee update to 2.5.1 (build 416) 2021-07-09 12:59:07 +08:00
crazywhalecc
d67dfe46f6 update to 2.5.0 (build 415) 2021-07-09 10:43:00 +08:00
crazywhalecc
e57cc43500 update to 2.5.0-b4 (build 414) 2021-07-09 02:15:04 +08:00
crazywhalecc
481063285b update some Docs and comments 2021-07-09 01:54:58 +08:00
crazywhalecc
d805523dbd update to 2.5.0-b3 (build 413) 2021-07-09 01:44:45 +08:00
crazywhalecc
58267f66fc update to 2.5.0-b3 (build 412) 2021-07-09 01:43:39 +08:00
crazywhalecc
48215f2e5e update to 2.5.0-b3 (build 411) 2021-07-09 01:39:45 +08:00
crazywhalecc
7e0fc1528a update to 2.5.0-b3 (build 410) 2021-07-09 01:38:30 +08:00
crazywhalecc
c185d20a93 update Docs 2021-07-04 18:02:03 +08:00
crazywhalecc
7ec847e576 update to 2.5.0-b2 (build 409) 2021-07-04 15:45:30 +08:00
jerry
4ee16d4fc6 update to 2.5.0-b1 (build 408) 2021-06-16 00:17:30 +08:00
Whale
59d614a24e Merge pull request #40 from sunxyw/patch-1
add latest config file format to quickstart-robot guide
2021-06-15 18:20:12 +08:00
sunxyw
40037b3f4b add highlight to quickstart-robot guide 2021-06-15 17:55:21 +08:00
sunxyw
f91c8b6205 add latest config file format to quickstart-robot guide
After the v1.0.0-beta2 update of go-cqhttp, the config file format has been converted from `hjson` to `yaml`. Update the guide.
See https://github.com/Mrs4s/go-cqhttp/releases/tag/v1.0.0-beta2
2021-06-15 17:46:00 +08:00
Whale
8f73a99ff7 Merge pull request #39 from YiwanGi/patch-1
Update global.php
2021-06-10 23:39:45 +08:00
Wang
e0f07cb396 Update global.php
🙃 强迫症 √
2021-06-10 17:45:27 +08:00
Whale
2950ab7472 Update README.md 2021-05-26 16:56:55 +08:00
Whale
0ab4053dfb Update README.md 2021-05-26 16:32:37 +08:00
Whale
745aa0f268 Update README.md 2021-05-26 16:31:41 +08:00
Whale
12bb93c2f0 Update README.md 2021-05-08 20:45:49 +08:00
95ca175901 Merge remote-tracking branch 'origin/master' 2021-05-08 10:06:17 +08:00
71585ed29d update to build 407
change daemon command from system to Process::kill
add master_pid for motd information
add option --preview
delete server_event_handler_class and use process
go-cqhttp-down.sh script add arm64 support
add ./zhamao support
change build-runtime.sh to install-runtime.sh
add option --disable-safe-exit
adjust some Console output
set start script using /bin/sh for supporting auto searching php and framework
2021-05-08 10:02:41 +08:00
Whale
231a377718 Update README.md 2021-05-08 01:24:27 +08:00
jerry
a80ee902a9 Merge remote-tracking branch 'origin/master' 2021-04-06 01:20:07 +08:00
jerry
c2d3b5f92a update to build 406 version 2021-04-06 01:19:56 +08:00
Whale
64365af124 Update README.md 2021-03-31 00:10:43 +08:00
jerry
60619dbffc update docs 2021-03-29 17:18:19 +08:00
jerry
77e77e9cc3 update to 2.4.4 version (build 405)
change requirements: add pcntl as required extension
update docs
2021-03-29 17:12:09 +08:00
jerry
d72b41a902 update to build 404
fix ./zhamao command
fix warning when first time starting framework
2021-03-29 15:48:47 +08:00
255 changed files with 11932 additions and 4435 deletions

View File

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

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

59
.github/workflows/integration-test.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Integration Test
on:
push:
branches:
- master
pull_request:
branches:
- master
types:
- opened
- synchronize
- reopened
- ready_for_review
- review_requested
jobs:
integration:
name: Integration Test (PHP ${{ matrix.php-versions }}) (OS ${{ matrix.operating-system }})
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: [ "ubuntu-latest", "macos-latest" ]
php-versions: [ "7.2", "7.3", "7.4", "8.0", "8.1" ]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: "shivammathur/setup-php@v2"
with:
php-version: ${{ matrix.php-versions }}
extensions: swoole, posix, json
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Validate composer.json
run: "composer validate --strict"
- name: Get composer cache directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer Dependencies
run: "composer install --prefer-dist --no-progress --optimize-autoloader"
- name: Run Static Analysis
run: "composer analyse"
- name: Run PHP CS Fixer Check
run: "./vendor/bin/php-cs-fixer fix --dry-run --diff"

33
.github/workflows/mkdocs-deploy.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: MkDocs Auto Deploy
on:
push:
branches:
- master
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v2
- name: Deploy docs to GitHub Pages
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: framework.zhamao.me
CONFIG_FILE: mkdocs.yml
EXTRA_PACKAGES: build-base
- name: Copy deployment to current folder
run: |
cp -r "${GITHUB_WORKSPACE}/site" "./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.ZHAMAO_XIN_TARGET }}

18
.gitignore vendored
View File

@@ -2,7 +2,6 @@
/src/test/
/src/webconsole/config/
/vendor/
zm.json
/zm_data/
composer.lock
/resources/server.phar
@@ -12,4 +11,19 @@ composer.lock
.phpunit.result.cache
.daemon_pid
/runtime/
/tmp/
/tmp/
/temp/
/site/
# 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
.zm_worker_*.pid
# Git Hook 的相关锁文件
cghooks.lock

75
.php-cs-fixer.php Normal file
View File

@@ -0,0 +1,75 @@
<?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,
])
->setFinder(
PhpCsFixer\Finder::create()
->exclude('vendor')
->exclude('docs')
->in(__DIR__ . '/src')
)
->setUsingCache(false);

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Reload Zhamao Server" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="./zhamao server:reload &amp;&amp; exit" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Zhamao Server" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="./zhamao server" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="./zhamao" />
<option name="SCRIPT_OPTIONS" value="server" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

17
.run/Run watcher.run.xml Normal file
View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run watcher" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="bin/watcher" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/zsh" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Stop Zhamao Server" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="./zhamao server:stop &amp;&amp; exit" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -7,18 +7,18 @@
[![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/OneBot-v11-success)](https://github.com/howmanybots/onebot)
![Integration Test](https://github.com/zhamao-robot/zhamao-framework/actions/workflows/integration-test.yml/badge.svg)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
[![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
</div>
## 开发者注意
开发者 QQ 群:**670821194** [点击加入群聊](https://jq.qq.com/?_wv=1027&k=YkNI3AIr)
当前 v2 版本已正式发布,此 master 分支为 2.0 版本,如需查看 v1 版本,请移步 `v1-legacy` 分支!
**如果有愿意一起开发框架本身的开发者,请提出 PR 或 Issue 参与开发!如果对框架本身的核心设计有更好的想法,可与作者成立开发组(目前仅作者 1 人),参与 OneBot V12 生态和框架本身的开发。**
2.0 版本如果有问题请第一时间加群反馈!
**相关正在进行的版本任务见 Projects 一栏!**
## 简介
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
@@ -41,30 +41,35 @@ public function index() {
```
## 开始
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。
如果你是初学者,可以直接使用以下脚本部署 PHP 环境和安装框架的脚手架:
```bash
# 新建一个自己喜欢名字的文件夹,运行一键安装脚本 (仅限 x86_64 和 aarch64 平台)
mkdir zhamao-app/
cd zhamao-app/
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
# 启动
vendor/bin/start server
```
## 文档v2 版本)
查看文档(国内自建):<https://docs-v2.zhamao.xin/>
查看文档(国内自建):<https://framework.zhamao.xin/>
备用链接(国外托管):<https://docs-v2.zhamao.me/>
备用链接(国外托管):<https://framework.zhamao.me/>
自行构建文档:`mkdocs build -d distribute`
## 特点
- 原生为多账号设计,支持多个机器人负载均衡
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
- 灵活的注解事件绑定机制
- 灵活的注解事件绑定机制,可兼容使用 PHP8 的 Attribute>=2.7 可用)
- 易用的上下文,模块内随处可用
- 采用模块化编写,可自由搭配其他 composer 组件
- 采用模块化编写,可自由搭配其他 composer 组件,也可单文件面向过程编写
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
- 静态文件服务器,可将前端合并到一起
## 从 v1 升级
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
- 自带 PHP + Swoole 环境无需手动编译安装by [crazywhalecc/static-php-cli](https://github.com/crazywhalecc/static-php-cli)
## 下载源码
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和 clone 仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
@@ -83,12 +88,10 @@ public function index() {
### 支付宝
![支付宝二维码](https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
作者的炸毛机器人已从2018年初起稳定运行了**四年半**,并且持续迭代。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ[627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。
@@ -100,4 +103,10 @@ public function index() {
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)
感谢 JetBrains 为此开源项目提供 PhpStorm 开发工具支持:
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/PhpStorm.svg" width="300">
感谢 [php-libonebot](https://github.com/botuniverse/php-libonebot) 开发者 @sunxyw 中为项目开发规范化提出的一些建议。
<!-- ![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg) -->

21
bin/phpunit-swoole Executable file → Normal file
View File

@@ -1,18 +1,20 @@
#!/usr/bin/env php
/** @noinspection ALL */<?php
<?php
/**
* Copyright: Swlib
* Author: Twosee <twose@qq.com>
* Date: 2018/4/14 下午10:58
*/
Co::set([
'log_level' => SWOOLE_LOG_INFO,
use Swoole\Coroutine;
Coroutine::set([
'log_level' => SWOOLE_LOG_INFO,
'trace_flags' => 0
]);
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
ini_set('date.timezone', 'Asia/Shanghai');
}
foreach ([
@@ -55,12 +57,13 @@ if (!defined('PHPUNIT_COMPOSER_INSTALL')) {
/** @noinspection PhpIncludeInspection */
require PHPUNIT_COMPOSER_INSTALL;
$starttime = microtime(true);
go(function (){
try{
go(function () {
try {
require_once __DIR__.'/../test/bootstrap.php';
PHPUnit\TextUI\Command::main(false);
} catch(Exception $e) {
echo $e->getMessage().PHP_EOL;
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
});
Swoole\Event::wait();
echo "Took ".round(microtime(true) - $starttime, 4). "s\n";
echo "Took " . round(microtime(true) - $starttime, 4) . "s\n";

View File

@@ -1,6 +1,34 @@
#!/usr/bin/env php
<?php /** @noinspection PhpIncludeInspection */
#!/bin/sh
require_once ((!is_dir(__DIR__ . '/../vendor')) ? getcwd() : (__DIR__ . "/..")) . "/vendor/autoload.php";
# shellcheck disable=SC2068
# shellcheck disable=SC2181
# author: crazywhalecc
# since: 2.5.0
(new ZM\ConsoleApplication("zhamao-framework"))->initEnv()->run();
if [ -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
result=$(echo "$1" | grep -E "module|build")
if [ "$result" != "" ]; then
executable="$executable -d phar.readonly=off"
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

118
bin/watcher Executable file
View File

@@ -0,0 +1,118 @@
#!/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,226 +0,0 @@
#!/usr/bin/env bash
_php_ver="7.4.16"
_libiconv_ver="1.15"
_openssl_ver="1.1.1j"
_swoole_ver="4.6.3"
_home_dir=$(pwd)"/"
function checkEnv() {
echo -n "检测核心组件... "
_msg="请通过包管理安装此依赖!"
type git >/dev/null 2>&1 || { echo "失败git 不存在!"$_msg; return 1; }
type gcc >/dev/null 2>&1 || { echo "失败gcc 不存在!"$_msg; return 1; }
type g++ >/dev/null 2>&1 || { echo "失败g++ 不存在!"$_msg; return 1; }
type unzip >/dev/null 2>&1 || { echo "失败unzip 不存在!"$_msg; return 1; }
type autoconf >/dev/null 2>&1 || { echo "失败autoconf 不存在!"; return 1; }
type pkg-config >/dev/null 2>&1 || { echo "失败pkg-config 不存在!"$_msg; return 1; }
type wget >/dev/null 2>&1 || type curl >/dev/null 2>&1 || { echo "失败curl/wget 不存在!"$_msg; return 1; }
echo "完成!"
echo "如果下载过程中出现错误,请删除 runtime/ 文件夹重试!"
echo "此脚本安装的php/swoole均为最小版本不含其他扩展如zip、xml、gd"
echo -n "如果编译过程缺少依赖,请通过包管理安装对应的依赖![按回车继续] "
# shellcheck disable=SC2034
read ents
}
function downloadIt() {
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
$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 downloadAll() {
# 创建文件夹
mkdir "$_home_dir""runtime" >/dev/null 2>&1
mkdir "$_home_dir""runtime/tmp_download" >/dev/null 2>&1
mkdir "$_home_dir""runtime/cellar" >/dev/null 2>&1
_down_dir=$_home_dir"runtime/tmp_download/"
# 下载PHP
echo -n "正在下载 php 源码... "
downloadIt "http://mirrors.sohu.com/php/php-$_php_ver.tar.gz" "$_down_dir""php.tar.gz" || { exit; }
# 下载libiconv
echo -n "正在下载 libiconv 源码... "
downloadIt "https://mirrors.tuna.tsinghua.edu.cn/gnu/libiconv/libiconv-$_libiconv_ver.tar.gz" "$_down_dir""libiconv.tar.gz" || { exit; }
echo -n "正在下载 openssl 源码... "
downloadIt "http://mirrors.cloud.tencent.com/openssl/source/openssl-$_openssl_ver.tar.gz" "$_down_dir""openssl.tar.gz" || { exit; }
echo -n "正在下载 swoole 源码... "
downloadIt "https://dl.zhamao.me/swoole/swoole-$_swoole_ver.tgz" "$_down_dir""swoole.tar.gz" || { exit; }
echo -n "正在下载 composer ... "
downloadIt "https://mirrors.aliyun.com/composer/composer.phar" "$_home_dir""runtime/cellar/composer" || { exit; }
#echo -n "正在下载 libcurl 源码... "
#downloadIt "https://curl.se/download/curl-7.75.0.tar.gz" "$_down_dir""libcurl.tar.gz" || { exit; }
}
function compileIt() {
_down_dir="$_home_dir""runtime/tmp_download/"
_source_dir="$_home_dir""runtime/tmp_source/"
_cellar_dir="$_home_dir""runtime/cellar/"
case $1 in
"libiconv")
if [ -f "$_cellar_dir""libiconv/bin/iconv" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libiconv.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libiconv-"$_libiconv_ver && \
./configure --prefix="$_cellar_dir""libiconv" >/dev/null 2>&1 && \
make -j4 >/dev/null 2>&1 && \
make install >/dev/null 2>&1 && \
echo "完成!"
;;
"libzip")
if [ -f "$_cellar_dir""libzip/bin/libzip" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libzip.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libzip-1.7.3" && \
./configure --prefix="$_cellar_dir""libzip" && \
make -j4 && \
make install && \
echo "完成!"
;;
"libcurl")
if [ -f "$_cellar_dir""libcurl/bin/libcurl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""libcurl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""libcurl-7.75.0" && \
./configure --prefix="$_cellar_dir""libcurl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"php")
if [ -f "$_cellar_dir""php/bin/php" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""php.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""php-"$_php_ver && \
./buildconf --force && \
PKG_CONFIG_PATH="$_cellar_dir""openssl/lib/pkgconfig" ./configure --prefix="$_cellar_dir""php" \
--with-config-file-path="$_home_dir""runtime/etc" \
--disable-fpm \
--enable-cli \
--enable-posix \
--enable-ctype \
--enable-mysqlnd \
--enable-pdo \
--enable-pcntl \
--with-openssl="$_cellar_dir""openssl" \
--enable-sockets \
--disable-xml \
--disable-xmlreader \
--disable-xmlwriter \
--without-libxml \
--disable-dom \
--without-sqlite3 \
--without-pdo-sqlite \
--disable-simplexml \
--with-pdo-mysql=mysqlnd \
--with-zlib \
--with-iconv="$_cellar_dir""libiconv" \
--enable-phar && \
make -j4 && \
make install && \
cp "$_source_dir""php-$_php_ver/php.ini-production" "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
"openssl")
if [ -f "$_cellar_dir""openssl/bin/openssl" ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""openssl.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""openssl-""$_openssl_ver" && \
./config --prefix="$_cellar_dir""openssl" && \
make -j4 && \
make install && \
echo "完成!"
;;
"swoole")
"$_home_dir"runtime/cellar/php/bin/php --ri swoole >/dev/null 2>&1
# shellcheck disable=SC2181
if [ $? == 0 ]; then
echo "已编译!" && return
fi
tar -xf "$_down_dir""swoole.tar.gz" -C "$_source_dir" && \
cd "$_source_dir""swoole-""$_swoole_ver" && \
PATH="$_cellar_dir""php/bin:$PATH" phpize && \
PATH="$_cellar_dir""php/bin:$PATH" ./configure --prefix="$_cellar_dir""php" \
--enable-sockets \
--enable-http2 \
--enable-openssl \
--with-openssl-dir="$_cellar_dir""openssl" \
--enable-mysqlnd && \
make -j4 && \
make install && \
echo "extension=swoole.so" >> "$_home_dir""runtime/etc/php.ini" && \
echo "完成!"
;;
esac
}
function compileAll() {
_down_dir=$_home_dir"runtime/tmp_download/"
_source_dir=$_home_dir"runtime/tmp_source/"
mkdir "$_source_dir" >/dev/null 2>&1
mkdir "$_home_dir""runtime/etc" >/dev/null 2>&1
echo -n "正在编译 libiconv ... "
compileIt libiconv || { return 1; }
#echo -n "正在编译 libcurl ... "
#compileIt libcurl || { exit; }
echo -n "正在编译 openssl ... "
compileIt openssl || { return 1; }
#echo -n "正在编译 libzip ... "
#compileIt libzip || { exit; }
echo -n "正在编译 php ... "
compileIt php || { return 1; }
echo -n "正在编译 swoole ... "
compileIt swoole || { return 1; }
return 0
}
function linkBin(){
mkdir "$_home_dir""runtime/bin" >/dev/null 2>&1
ln -s "$_home_dir""runtime/cellar/php/bin/php" "$_home_dir""runtime/bin/php" >/dev/null 2>&1
echo "runtime/cellar/php/bin/php runtime/cellar/composer \$@" > "$_home_dir""runtime/bin/composer" && chmod +x "$_home_dir""runtime/bin/composer"
echo "Done!"
runtime/bin/composer config repo.packagist composer https://mirrors.aliyun.com/composer/
}
checkEnv && \
downloadAll && \
compileAll && \
linkBin && \
echo "成功部署所有环境!" && \
echo -e "composer更新依赖\t\"runtime/bin/composer update\"" && \
echo -e "启动框架(源码模式)\t\"runtime/bin/php bin/start server\"" && \
echo -e "启动框架(普通模式)\t\"runtime/bin/php vendor/bin/start server\""

View File

@@ -4,9 +4,22 @@
"minimum-stability": "stable",
"license": "Apache-2.0",
"extra": {
"exclude_annotate": [
"src/ZM"
]
"zm": {
"exclude-annotation-path": [
"src/ZM"
]
},
"hooks": {
"post-merge": "composer install",
"pre-commit": [
"echo committing as $(git config user.name)",
"./vendor/bin/php-cs-fixer fix --dry-run --diff ./src"
],
"pre-push": [
"./vendor/bin/php-cs-fixer fix --dry-run --diff ./src",
"composer analyse"
]
}
},
"authors": [
{
@@ -17,26 +30,34 @@
"prefer-stable": true,
"bin": [
"bin/start",
"bin/phpunit-swoole"
"bin/phpunit-swoole",
"bin/watcher"
],
"require": {
"php": ">=7.2",
"php": "^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1",
"ext-json": "*",
"ext-posix": "*",
"doctrine/annotations": "~1.10",
"psy/psysh": "@stable",
"symfony/polyfill-ctype": "^1.20",
"symfony/polyfill-mbstring": "^1.20",
"symfony/console": "^5.1",
"symfony/routing": "^5.1",
"zhamao/console": "^1.0.10",
"doctrine/annotations": "~1.12 || ~1.4.0",
"doctrine/dbal": "^2.13.1",
"jelix/version": "^2.0",
"koriym/attributes": "^1.0",
"league/climate": "^3.6",
"psy/psysh": "^0.11.2",
"symfony/console": "~5.0 || ~4.0 || ~3.0",
"symfony/polyfill-ctype": "^1.19",
"symfony/polyfill-mbstring": "^1.19",
"symfony/polyfill-php80": "^1.16",
"symfony/routing": "~5.0 || ~4.0 || ~3.0",
"zhamao/config": "^1.0",
"zhamao/request": "^1.1",
"zhamao/connection-manager": "^1.0"
"zhamao/connection-manager": "^1.0",
"zhamao/console": "^1.0",
"zhamao/request": "^1.1"
},
"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"
},
"autoload": {
@@ -47,7 +68,28 @@
"src/ZM/global_functions.php"
]
},
"autoload-dev": {
"psr-4": {
"Module\\": "src/Module",
"Custom\\": "src/Custom"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"require-dev": {
"swoole/ide-helper": "@dev"
"brainmaestro/composer-git-hooks": "^2.8",
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",
"phpstan/phpstan": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0",
"swoole/ide-helper": "^4.5"
},
"scripts": {
"post-install-cmd": [
"[ $COMPOSER_DEV_MODE -eq 0 ] || vendor/bin/cghooks add"
],
"analyse": "phpstan analyse --memory-limit 300M -l 0 ./src",
"cs-fix": "php-cs-fixer fix $1"
}
}
}

View File

@@ -1,130 +1,152 @@
<?php
/** @noinspection PhpFullyQualifiedNameUsageInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
/** bind host */
declare(strict_types=1);
/* bind host */
$config['host'] = '0.0.0.0';
/** bind port */
/* bind port */
$config['port'] = 20001;
/** 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = "http://127.0.0.1:" . $config['port'];
/* 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = 'http://127.0.0.1:' . $config['port'];
/** 框架是否启动debug模式 */
/* 框架是否启动debug模式当debug模式为true时启用热更新需要安装inotify扩展 */
$config['debug_mode'] = false;
/** 存放框架内文件数据的目录 */
$config['zm_data'] = realpath(__DIR__ . "/../") . '/zm_data/';
/* 存放框架内文件数据的目录 */
$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参数 */
/* 对应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
// '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
'max_wait_time' => 5,
// 'task_worker_num' => 4,
// 'task_enable_coroutine' => true
];
/** 轻量字符串缓存,默认开启 */
$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['sql_config'] = [
'sql_host' => '',
'sql_port' => 3306,
'sql_username' => 'name',
'sql_database' => 'db_name',
'sql_password' => '',
'sql_options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false
/* 一些框架与框架运行时设置的调整 */
$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' => [],
],
'sql_no_exception' => false,
'sql_default_fetch_mode' => PDO::FETCH_ASSOC // added in 1.5.6
];
/** Redis连接信息host留空则启动时不创建Redis连接池 */
/* 轻量字符串缓存,默认开启 */
$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' => ''
'auth' => '',
];
/** onebot连接约定的token */
$config["access_token"] = '';
/* onebot连接约定的token */
$config['access_token'] = '';
/** HTTP服务器固定请求头的返回 */
/* HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'Server' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
'Content-Type' => 'text/html; charset=utf-8',
];
/** HTTP服务器在指定状态码下回复的页面默认 */
/* HTTP服务器在指定状态码下回复的页面默认 */
$config['http_default_code_page'] = [
'404' => '404.html'
'404' => '404.html',
];
/** zhamao-framework在框架启动时初始化的atomic们 */
/* zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
//'custom_atomic_name' => 0, //自定义添加的Atomic
// 'custom_atomic_name' => 0, //自定义添加的Atomic
];
/** 终端日志显示等级0-4 */
$config["info_level"] = 2;
/* 终端日志显示等级0-4 */
$config['info_level'] = 2;
/** 上下文接口类 implemented from ContextInterface */
/* 上下文接口类 implemented from ContextInterface */
$config['context_class'] = \ZM\Context\Context::class;
/** 静态文件访问 */
/* 静态文件访问 */
$config['static_file_server'] = [
'status' => false,
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
'document_root' => realpath(__DIR__ . '/../') . '/resources/html',
'document_index' => [
'index.html'
]
'index.html',
],
];
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
// 这里添加例如 \ZM\Event\ServerEventHandler::class 这样的启动注解类
];
/** 机器人解析模块关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
/* 机器人解析模块关闭后无法使用如CQCommand等注解(上面的modules即将废弃) */
$config['onebot'] = [
'status' => true,
'single_bot_mode' => false,
'message_level' => 99999
'message_level' => 99,
'message_convert_string' => true,
'message_command_policy' => 'interrupt',
];
/** 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */
/* 一个远程简易终端使用nc直接连接即可但是不建议开放host为0.0.0.0(远程连接) */
$config['remote_terminal'] = [
'status' => false,
'host' => '127.0.0.1',
'port' => 20002,
'token' => ''
'token' => '',
];
/* 模块(插件)加载器的相关设置 */
$config['module_loader'] = [
'enable_hotload' => false,
'load_path' => $config['zm_data'] . 'modules',
];
return $config;

View File

@@ -1,54 +0,0 @@
# FAQ
这里会写一些常见的疑难解答。
## 启动时报错 Address already in use
1. 检查是否开启了两次框架,每个端口只能开启一个框架。
2. 如果是之前已经在 20001 端口或者你设置了别的应用同样占用此端口,更换配置文件 `global.php` 中的 port 即可。
3. 如果是之前框架成功启动,但是使用 Ctrl+C 停止后再次启动导致的报错,请根据下面的步骤来检查是否存在僵尸进程。
- 如果系统内装有 `htop`,可以直接在 `htop` 中开启 Tree 模式并使用 filter 过滤 php检查残留的框架进程。
- 如果系统没有 `htop`,使用 `ps aux | grep vendor/bin/start | grep -v grep` 如果存在进程,请使用以下命令尝试杀掉:
```bash
# 如果确定框架的数据都已保存且没有需要保存的缓存数据,直接杀掉 SIGKILL 即可,输入下面这条
ps aux | grep vendor/bin/start | grep -v grep | awk '{print $2}' | xargs kill -9
# 如果不确定框架是不是还继续运行,想尝试正常关闭(走一遍储存保存数据的事件),使用下面这条
# 首先使用 'ps aux | grep vendor/bin/start | grep -v grep' 找到进程中第二列最小的pid
# 然后使用下面的这条命令假设最小的pid是23643
kill -INT 23643
# 如果使用 ps aux 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死
```
## 出现 deadlock 字样
一般情况下,如果误操作框架可能会报如下图的错误:
```
===================================================================
[FATAL ERROR]: all coroutines (count: 1) are asleep - deadlock!
===================================================================
[Coroutine-1]
--------------------------------------------------------------------
#0 Swoole\Coroutine\System::sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/global_functions.php:232]
#1 zm_sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/Module/Example/Hello.php:38]
#2 Module\Example\Hello->onStart() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:205]
#3 ZM\Event\EventDispatcher->dispatchEvent() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:89]
#4 ZM\Event\EventDispatcher->dispatchEvents() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/SwooleEvent/OnWorkerStart.php:130]
#5 ZM\Event\SwooleEvent\OnWorkerStart->onCall() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Framework.php:336]
```
这种错误的出现原因一般是因为协程未结束而 Worker 进程提前退出导致的,这个错误也可手动造成(在任意 Worker 进程内的位置使用 `zm_yield()` 且不使用 `zm_resume()` 恢复,期间使用 reload 或 stop 重启或停止框架就会报错)。
还有一种情况是数据库、文件读取或下载上传还没有传送结束,时间已经超时,在关闭或重启框架时不得不强行切断协程的运行。这种情况建议根据下方的打印输出栈进行插错,建议将协程运行时间长的过程缩短或调长 `swoole` 配置项下面的 `max_wait_time` 时间2.4.3 版本起此参数默认为 5 秒。
## 使用 LightCache 关闭时无法正常保存持久化
LightCache 因为是跨内存使用的,所以每次重启和关闭框架时,都只会让其中一个进程去保存。因为在 2.4.2 版本开始,持久化的逻辑发生了更改,不再支持 `expire = -2` 进行设置持久化(因为那样会很容易让开发者写错),仅支持使用 `LightCache::addPersistence($key)` 这样的方式进行设置持久化,所以在 2.4.2 版本以后,请使用此方法进行持久化设置,保证数据不丢失。
此外2.4.2 版本起,不再支持用户手动调用 `savePersistence()` 方法,普通用户不可手动调用此方法,否则会导致数据出错。
##

View File

@@ -0,0 +1,88 @@
# 接入青云客智能聊天机器人API
作为一个群聊机器人,懂得聊天会让机器人增色不少,在大数据和 AI 热潮下,不少厂商都研发了自己的智能聊天 API例如图灵机器人、腾讯智能闲聊等大厂开发的 API 自然有着他人无可比拟的健壮性和可靠性,但是随之而来不菲的价格显然并不适合大众开发者。这时候一个免费、可用的智能聊天 API 便非常重要了,其中,青云客是少有的完全免费、无需注册的智能聊天 API提供了包括智能聊天、歌词、天气查询、笑话等多种有用功能且接入简单非常适合新手开发者尝试。
## 结果演示
![圖片](https://user-images.githubusercontent.com/31698606/158875192-108698a3-b54e-4fc0-889a-0829ca328b13.png)
## 阅读接入指南
不管接入何种服务,阅读接入指南永远都是最优先、最重要的一步,所幸青云客的接入指南十分简单,简单来说归纳为以下:
* 请求GET https://api.qingyunke.com/api.php
* 参数:
* * `key` 目前固定为 `free`
* * `appid` 目前固定为 `0`
* * `msg` 关键词,需要经过 `urlencode`
* 注意:返回结果中 `{br}` 代表换行
## 逻辑编写
阅读过后,我们便可以进行主要的编写工作了。
首先,为了机器人的性能考虑,也为了避免过分打扰群聊的聊天,我们希望机器人只有在主动触发(@AT 或者 关键词等)时才会进行智能聊天。
对于关键词匹配,我们可以使用 `@CQCommand`
```php
/**
* 智能聊天
*
* @CQCommand(start_with="机器人")
*/
public function chat()
{
// 替换掉机器人前缀,并获取消息内容
$msg = ctx()->getMessage();
$msg = str_replace('机器人', '', $msg);
if (empty(trim($msg))) {
$msg = ctx()->getFullArg('怎么了?');
}
Console::info('正在获取智能聊天回复:' . $msg);
// 请求 API 获取回复
$raw_data = file_get_contents('https://api.qingyunke.com/api.php?key=free&appid=0&msg=' . urlencode($msg));
try {
$data = json_decode($raw_data, true, 512, JSON_THROW_ON_ERROR);
} catch (\Exception $e) {
$data = ['content' => '机器人解析异常,请稍后再试'];
Console::warning('无法获取智能聊天回复:' . $e->getMessage());
}
if ($data['result'] !== 0) {
$data = ['content' => '机器人服务异常,请稍后再试'];
Console::warning('无法获取智能聊天回复:' . $raw_data);
}
Console::info('获取智能聊天回复完成:' . $data['content']);
// 将 {br} 替换为换行
$data['content'] = strtr($data['content'], ['{br}' => "\n"]);
return $data['content'];
}
```
这样我们的命令便只会在用户发送以`机器人`开头的消息时才会触发。
同时,我们也希望在 @AT 机器人时也进行回复,此时可以使用 `@CQBefore` 方法进行折中:
```php
/**
* 将 AT 机器人的消息交由智能聊天处理
*
* @CQBefore("message")
*/
public function changeAt(): bool
{
// 判断此条消息是否 AT 了机器人
if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) {
// 将 AT 本身从消息中去掉
$msg = str_replace(CQ::at(ctx()->getRobotId()), '', ctx()->getMessage());
ctx()->setMessage('机器人' . trim($msg));
// 调用智能聊天
ctx()->reply($this->chat());
return false;
}
return true;
}
```

View File

@@ -0,0 +1,112 @@
# 基于词性分析和魅族天气的天气查询机器人
本文将基于 [`jieba-php`](https://github.com/fukuball/jieba-php) 中文分词库以及 [魅族天气 API](https://github.com/shichunlei/-Api/blob/master/MeizuWeather.md) 开发一个天气查询机器人。
## 结果演示
![圖片](https://user-images.githubusercontent.com/31698606/159122016-61ba9696-5786-4561-b371-827d9f1d01aa.png)
尾部的随机表情并非本教程的一部分。
## 逻辑编写
[`jieba-php`](https://github.com/fukuball/jieba-php) 是目前比较好用的中文分词库,虽然最近的维护并不活跃,但已足够我们的需求:
```shell
composer require fukuball/jieba-php:dev-master
```
以下代码使用了本文作者自行编写的天气查询库,需要进行引入:
```shell
composer require sunxyw/weather
```
您也可以将以下代码自行改写为直接调用魅族天气 API详情请参阅[魅族天气 API 文档](https://github.com/shichunlei/-Api/blob/master/MeizuWeather.md)。
```php
<?php
namespace Bot\Module\SmartChat;
use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Posseg;
use Sunxyw\Weather\Weather;
use ZM\Annotation\CQ\CQCommand;
use ZM\Console\Console;
class WeatherReport
{
/**
* 加载字典
*
* @OnStart(worker_id=-1)
*
* @return void
*/
public function initDictionary(): void
{
// 分词以及词性分析需要载入字典到内存
ini_set('memory_limit', '600M');
Jieba::init(['dict' => 'small']);
Posseg::init();
}
/**
* 查询天气
*
* @CQCommand(keyword="天气")
*
* @return string
*/
public function cmdQueryWeather(): string
{
// 分词并进行词性分析
$seg_list = Posseg::cut(ctx()->getMessage());
$tags = array_column($seg_list, 'tag');
// 找出词性为 ns地名的单词
$location_index = array_search('ns', $tags, true);
$location = $seg_list[$location_index]['word'];
// 此处引入了本文作者自己写的天气库
$w = new Weather();
try {
$report = $w->getWeather($location);
} catch (\InvalidArgumentException) {
return '城市输入错误';
} catch (\JsonException $e) {
Console::warning("天气查询失败:{$e->getMessage()}");
return '天气查询失败';
}
$template = <<<EOF
%s天气%s
温度:%s℃
湿度:%s%%
风向:%s %s
空气质量:%s
------------------------------
未来三天天气:
%s%s日间%s℃夜间%s℃吹%s %s
%s%s日间%s℃夜间%s℃吹%s %s
%s%s日间%s℃夜间%s℃吹%s %s
EOF;
$args = [
$report->getCity(),
$report->getRealtime()['weather'],
$report->getRealtime()['temperature'],
$report->getRealtime()['humidity'],
$report->getRealtime()['wind_direction'],
$report->getRealtime()['wind_speed'],
$report->getRealtime()['air_quality'],
];
foreach (array_slice($report->getForecastDaily(), 0, 3) as $forecast) {
$args[] = $forecast['date'];
$args[] = $forecast['weather'];
$args[] = $forecast['temperature']['day'];
$args[] = $forecast['temperature']['night'];
$args[] = $forecast['wind_direction'];
$args[] = $forecast['wind_speed'];
}
return vsprintf($template, ...$args);
}
}
```

View File

@@ -0,0 +1,3 @@
# 手动部署环境教程
TODO: 还没写

View File

@@ -0,0 +1 @@
.hljs{display:block;overflow-x:auto;padding:.5em;background:#f0f0f0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#bc6060}.hljs-literal{color:#78a960}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}

View File

@@ -42,6 +42,21 @@ MessageUtil::containsImage("[CQ:image,file=a.jpg,url=http://xxx]"); // true
MessageUtil::containsImage("[CQ:face,id=140] 咦,这是一条带表情的消息"); // false
```
### isAtMe()
检查消息中是否含有@bot的消息
定义:`isAtMe($msg, $me_id)`
参数 `$me_id` 为Bot的QQ号。
返回:`bool`true 就是有false 就没有。
```php
MessageUtil::isAtMe("[CQ:at,qq=123456]炸毛你好","123456"); // true
MessageUtil::isAtMe("[CQ:at,qq=123456789]另一个朋友你好","123456"); // false
```
### getImageCQFromLocal()
通过文件路径获取图片的发送 CQ 码。
@@ -87,3 +102,78 @@ MessageUtil::splitCommand("我有 三个空格"); // ["我有","三个空格"]
返回:`\ZM\Entity\MatchObject` 对象,含有匹配成功与否,匹配到的注解对象,匹配到的分割词等,见 []
### addShortCommand()
快速添加一条静态消息回复命令。
定义:`addShortCommand($command, string $reply)`
参数 `$command` 为问的内容,如 `炸毛不聪明`
参数 `$reply` 为回复的内容,如 `其实还是很聪明的!`
这个命令推荐在 `@OnStart` 注解下使用,可以用这个来做一个动态的词库,从文件加载后使用。
```php
/**
* @OnStart()
*/
public function onStart() {
MessageUtil::addShortCommand("炸毛不聪明", "其实还是很聪明的!");
}
```
<chat-box>
) 炸毛不聪明
( 其实还是很聪明的!
</chat-box>
### strToArray()
`string` 类型的消息文本转换为 `array` 格式。
定义:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
参数 `$msg` 为带 OB/CQ 码的字符串消息,如 `你好啊,[CQ:at,qq=123]`
参数 `$ignore_space``false` 时,转换的数组内会包含空 `text` 段。
参数 `$trim_text``true` 时,会自动去除 `text` 段消息头尾的换行符和空格。
这个命令转换的数组格式符合 OneBot 11/12 标准,但细节上可能会与不同 OneBot 实现有所差异。
```php
$str = "你好啊,[CQ:at,qq=123]";
$arr = \ZM\Utils\MessageUtil::strToArray($str);
```
转换结果参考如下:
```json
[
{
"type": "text",
"data": {
"text": "你好啊,"
}
},
{
"type": "at",
"data": {
"qq": "123"
}
}
]
```
### arrayToStr()
`array` 格式的消息内容转换为字符串 + CQ 码的形式。
定义:`arrayToStr(array $array)`
```php
// 我们使用上边的 $arr 作为传入值。
$new_str = \ZM\Utils\MessageUtil::arrayToStr($arr);
// 结果:"你好啊,[CQ:at,qq=123]"
```

View File

@@ -0,0 +1,9 @@
# 机器人 APIOneBotV12待发布
!!! tip "提示"
目前由于 OneBot 12 标准还没有定稿,处于草案阶段,故框架暂不更新。
在未来升级到 OneBot 12 标准后,框架会提供转换及兼容措施以及 12 版本的 API 方法。
见 [机器人动作OneBot 11](../robot-api)。

View File

@@ -1,26 +1,29 @@
# 机器人 APIZMRobot
# 机器人 APIOneBotV11
ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
OneBotV11 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
| 属性项 | 属性值 | 备注 |
| -------- | ---------------- | ------------------------------ |
| 名称 | ZMRobot | |
| 类型 | 实例化类 | `$r = new ZMRobot($conn)` |
| 命名空间 | `ZM\API\ZMRobot` | 使用前先 `use ZM\API\ZMRobot;` |
| 属性项 | 属性值 | 备注 |
| -------- | ------------------ | -------------------------------- |
| 名称 | OneBotV11 | |
| 类型 | 实例化类 | `$r = new OneBotV11($conn)` |
| 命名空间 | `ZM\API\OneBotV11` | 使用前先 `use ZM\API\OneBotV11;` |
| 别名 | `ZM\API\ZMRobot` | 此类目前是 `extends OneBotV11` |
> 你也可以继续使用 2.5 版本之前的别名类 `ZMRobot`,但未来框架将会优先兼容 OneBot V12 版本的协议,可能会造成更新问题,建议切换为 OneBotV11 类。
## 属性
对象属性方法是对 API 的调整,例如是否以 `_async``_rate_limited` 后缀发送 API、设置协程返回还是异步返回结果等。
### ZMRobot::API_NORMAL
### OneBotV11::API_NORMAL
以默认(无后缀)方式请求 API。
### ZMRobot::API_ASYNC
### OneBotV11::API_ASYNC
以后缀 `_async` 方式异步请求 API。
### ZMRobot::API_RATE_LIMITED
### OneBotV11::API_RATE_LIMITED
以后缀 `_rate_limited` 方式请求 API。
@@ -30,7 +33,7 @@ ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机
设置后缀。目前支持 `_async``_rate_limited`
- **prefix**: `int` `默认:API_NORMAL`,可选 `ZMRobot::API_NORMAL``ZMRobot::API_ASYNC``ZMRobot::API_RATE_LIMITED`
- **prefix**: `int` `默认:API_NORMAL`,可选 `OneBotV11::API_NORMAL``OneBotV11::API_ASYNC``OneBotV11::API_RATE_LIMITED`
设置后缀后,请求的 API 会发生变化。例如发送私聊消息:`sendPrivateMsg()`,请求的 API 为 `send_private_msg_async`,详见 [OneBot 文档](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/README.md)。
@@ -43,22 +46,22 @@ ZMRobot 类是封装好的 OneBot 标准的 API 接口调用类,可以在机
获取当前对象的机器人 QQ 或 OneBot 实例的 ID。
```php
$bot = ZMRobot::get(123456);
$bot = OneBotV11::get(123456);
echo $bot->getSelfId(); //123456
```
### ZMRobot::get()
### OneBotV11::get()
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 ZMRobot 对象。
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 OneBotV11对象。
参数:`$robot_id`,必填。
```php
$r = ZMRobot::get(123456);
$r = OneBotV11::get(123456);
$r->sendPrivateMsg(55555, "hello");
```
### ZMRobot::getRandom()
### OneBotV11::getRandom()
静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。
@@ -66,21 +69,21 @@ $r->sendPrivateMsg(55555, "hello");
```php
try {
$bot = ZMRobot::getRandom();
$bot = OneBotV11::getRandom();
$bot->sendPrivateMsg(55555, "foo");
} catch (\ZM\Exception\RobotNotFoundException $e) {
echo "还没有机器人连接到框架!\n";
}
```
### ZMRobot::getAllRobot()
### OneBotV11::getAllRobot()
获取所有连接到框架的机器人的 ZMRobot 对象。
获取所有连接到框架的机器人的 OneBotV11 对象。
返回值:`ZMRobot[]`
返回值:`OneBotV11[]`
```php
$all = ZMRobot::getAllRobot();
$all = OneBotV11::getAllRobot();
foreach($all as $v) {
$v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!");
}
@@ -95,7 +98,7 @@ foreach($all as $v) {
```php
//从上下文获取 Websocket 连接对象
$conn = ctx()->getConnection();
$bot = new ZMRobot($conn);
$bot = new OneBotV11($conn);
```
## 返回结果处理
@@ -103,7 +106,7 @@ $bot = new ZMRobot($conn);
因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID以供我们核查消息和后续撤回等操作需要。
```php
$bot = ZMRobot::get("123456"); // 机器人QQ号
$bot = OneBotV11::get("123456"); // 机器人QQ号
$obj = $bot->sendGroupMsg("234567", "你好");
echo json_encode($obj, 128|256);
```
@@ -163,7 +166,7 @@ vardump($result["retcode"]); //如果成功撤回,输出 int(0)
=== "代码"
```php
$bot = ZMRobot::get(123456); // 123456是你的机器人QQ
$bot = OneBotV11::get(123456); // 123456是你的机器人QQ
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
```
@@ -766,6 +769,21 @@ vardump($result["retcode"]); //如果成功撤回,输出 int(0)
响应数据:无
### getExtendedAPI()
用来调用 OneBot 标准之外扩展出来的自定义 API。与下方 `callExtendedAPI` 不同的是,为了方便用户使用,炸毛框架内置了热门使用并且相对稳定的机器人客户端的专有 API。
目前内置了 `go-cqhttp` 频道相关的扩充 API。
使用示例:`getExtendedAPI('go-cqhttp')->getGuildList()`
使用示例2`getExtendedAPI()->sendGuildChannelMsg($guild_id, $channel_id, '频道的消息')`
唯一一个参数做保留,用于选择不同客户端,目前仅支持 `go-cqhttp`,所以缺省也默认为 `go-cqhttp`
!!! warning "注意"
由于不同版本的扩展 API 变化可能会很大,改动较多,炸毛框架不会将对应扩展方法写入文档,具体调用情况可根据 IDE 自动补全中的文档或对应类的注释查看。
### callExtendedAPI() (扩充 API
用来调用 OneBot 标准之外扩展出来的自定义 API。

View File

@@ -0,0 +1,86 @@
# 图灵机器人 APITuringAPI
类定义:`\ZM\API\TuringAPI`
## 方法
### TuringAPI::getTuringMsg()
请求图灵接口,返回回复的消息。
定义:`getTuringMsg($msg, $user_id, $api)`
参数 `$msg` 为用户的消息内容,如果含有图片 CQ 码,则自动转换为图灵兼容的接口模式。
参数 `$user_id` 为用户 ID一般默认给 QQ 号码就可以了,注意最好不要有特殊字符(如 `./\<>*` 等),否则会间断性调用失败。
参数 `$api` 为图灵机器人的 `apikey`,可以到 <http://www.turingapi.com/> 申请免费或付费的 API key。
在框架的示例模块中,已经写好了一个正常机器人响应图灵回复的命令,如下:
```php
class Hello {
/**
* 图灵机器人的内置实现在www.turingapi.com申请一个apikey填入下方变量即可。
* @CQCommand(start_with="机器人",end_with="机器人",message_type="group")
* @CQMessage(message_type="private",level=1)
*/
public function turingAPI() {
$user_id = ctx()->getUserId();
$api = ""; // 请在这里填入你的图灵机器人的apikey
if ($api === "") return false; //如果没有填入apikey则此功能关闭
if (($this->_running_annotation ?? null) instanceof CQCommand) {
$msg = ctx()->getFullArg("我在!有什么事吗?");
} else {
$msg = ctx()->getMessage();
}
ctx()->setMessage($msg);
if (MessageUtil::matchCommand($msg, ctx()->getData())->status === false) {
return TuringAPI::getTuringMsg($msg, $user_id, $api);
} else {
QQBot::getInstance()->handle(ctx()->getData(), ctx()->getCache("level") + 1);
return true;
}
}
/**
* 响应at机器人的消息
* @CQBefore("message")
*/
public function changeAt() {
if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) {
$msg = str_replace(CQ::at(ctx()->getRobotId()), "", ctx()->getMessage());
ctx()->setMessage("机器人" . $msg);
Console::info(ctx()->getMessage());
}
return true;
}
}
```
如上述代码,我们将申请的 apikey 填入变量 `$api` 中,启动机器人即可使用,以下是实测消息(我用自己申请的 key 做测试回复的消息)。
<chat-box>
) 你咋了
( 我没事哦,谢谢您的关心。
) 上海天气
( 上海:周一 03月29日,小雨 东南风转东风,最低气温14度最高气温24度。
^ 切换为群内
) 机器人
( 我在!有什么事吗?
) 你叫啥
( 我的名字叫炸毛,认识你很高兴呢!
</chat-box>
在默认示例模块中的例子是直接可以拿来用的,这段代码同时做了对 at 的处理、以及兼容用户自定义写的其他命令的方式,下面是默认模块填好 apikey 后可以用的各种方式提问:
<chat-box>
^ 切换为群内
) 我是一条普通消息,这条机器人不会回复我
) @机器人 你叫啥
( 我是聪明可爱的炸毛,认识你很高兴。
) 机器人
( 我在!有什么事吗?
) 一言
( 多少事,从来急,天地转,光阴迫,一万年太久,只争朝夕。
</chat-box>

View File

@@ -124,7 +124,7 @@ public function ping() {
## getRobot() - 获取机器人 API 对象
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](../bot/robot-api.md)。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。

View File

@@ -0,0 +1,104 @@
# 事件跟踪器及调试
众所周知炸毛框架中的事件由内置的事件分发器EventDispatcher负责分发但调试事件分发在之前的版本比较困难例如不能获取到事件如何被调用以及事件如何被捕获。
EventTracer 的作用是记录事件的调用顺序,以便于调试。
命名空间使用指南:`use ZM\Event\EventTracer;`
## EventTracer::getCurrentEvent() - 获取当前注解事件对象
```php
/**
* @OnStart()
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEvent());
}
/*
^ ZM\Annotation\Swoole\OnStart^ {#192
+worker_id: 0
+method: "onStart"
+class: "Module\Example\Hello"
}
*/
```
这里这个方法必须在注解事件内执行,如果在注解事件外执行,将会返回 `null`
## EventTracer::getCurrentEventMiddlewares() - 获取当前注解事件的中间件们
```php
/**
* @OnStart()
* @Middleware("timer")
*/
public function onStart() {
zm_dump(EventTracer::getCurrentEventMiddlewares());
}
/*
^ array:1 [
0 => ZM\Annotation\Http\Middleware^ {#194
+middleware: "timer"
+params: []
+method: "onStart"
+class: "Module\Example\Hello"
}
]
*/
```
返回值为当前注解事件的中间件们,如果没有注解中间件,返回 `[]`
## EventTracer::getEventTraceList() - 获取注解事件的列表
此处返回的是 `getCurrentEvent()` 相同的对象,但是返回的是一个数组,数组中的元素是注解事件。
```php
/**
* 一个简单随机数的功能demo
* 问法1随机数 1 20
* 问法2从1到20的随机数
* @CQCommand("随机数")
* @Middleware("timer")
* @CQCommand(pattern="*从*到*的随机数")
* @return string
*/
public function randNum() {
// 此处为随机数代码
zm_dump(EventTracer::getEventTraceList());
return "随机数:" . rand(1, 20);
}
/*
^ array:2 [
0 => ZM\Annotation\CQ\CQCommand^ {#193
+match: ""
+pattern: "*从*到*的随机数"
+regex: ""
+start_with: ""
+end_with: ""
+keyword: ""
+alias: []
+message_type: ""
+user_id: 0
+group_id: 0
+discuss_id: 0
+level: 20
+method: "randNum"
+class: "Module\Example\Hello"
}
1 => ZM\Annotation\Swoole\OnMessageEvent^ {#165
+connect_type: "default"
+rule: "connectIsQQ()"
+level: 99
+method: "handleByEvent"
+class: "ZM\Module\QQBot"
}
]
*/
```
## EventDispatcher::enableEventTrace() - 启用事件跟踪器
还没写完,不着急。

View File

@@ -4,6 +4,8 @@
## getClassPath()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L24)
根据加载的用户编写的代码类名来获取类所在的文件路径。
=== "src/Module/Example/Hello.php"
@@ -38,6 +40,8 @@
## explodeMsg()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L39)
切割字符串的函数支持多空格换行tab。
定义:`explodeMsg($msg, $ban_comma = false)`
@@ -49,6 +53,8 @@ echo json_encode($s, 128|256); // ["你好啊","你好你好","我还有多个
## unicode_decode()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L54)
Unicode 解码,一般用于被转义的 Unicode 转回来。
```php
@@ -57,6 +63,8 @@ echo unicode_decode("\u4f60\u597d"); // 你好
## matchPattern()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L91)
根据星号匹配字符串(非正则表达式)。
匹配示例:
@@ -81,6 +89,8 @@ matchPattern("*把*翻译成*", "请把你好翻译成阿拉伯语"); // true
## split_explode()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L103)
`explodeMsg()` 类似,用作分割字符串,不过此函数加入了对 `中文|数字` 两者的分割,也就是说中文和数字之间也会被分割。
定义:`split_explode($del, $str, $divide_en = false)`
@@ -97,6 +107,8 @@ split_explode(" ", "前进20 急啊急啊"); // ["前进","20","急啊急啊"]
## matchArgs()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L135)
`matchPattern()` 的扩展,如果 `matchPattern()` 格式的字符串和模式匹配成功,则通过星号位置来提取星号匹配到的内容,参数同 `matchPattern()`
```php

View File

@@ -2,7 +2,7 @@
此类管理的是 TaskWorker 相关工作。有关使用 TaskWorker 的教程,见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)
类定义:`\ZM\Utils\TaskManager`
类定义:`\ZM\Utils\Manager\TaskManager`
使用 TaskWorker 需要先在 `global.php` 配置文件中开启!

View File

@@ -0,0 +1,243 @@
# 模块打包
从 2.5 版本起,炸毛框架的模块源码支持了打包和分发,开发者可以通过将自己的功能编写打包,并通过互联网进行分发,供其他人使用。此外,还提供了模块包热加载(不解包直接运行)和模块包解包功能。
## 构建模块包配置文件
炸毛框架的模块区分是根据 `src` 目录下的文件夹定义的,模块包的配置文件命名必须为 `zm.json`,此外,假设我们编写了一个最简单的模块,以脚手架生成的 Example 模块为例,文件夹结构如下:
```
src/
└── Module/
   ├── Example/
   │   ├── Hello.php
   │   └── zm.json
   └── Middleware/
   └── TimerMiddleware.php
```
我们在 Example 目录下创建一个 `zm.json` 的文件,编写配置,即代表 `src/Module/Example/` 文件夹及里面的用户模块源码为一个模块包,也就可以被框架识别并打包处理。
编写的配置文件结构如下:
```
{
"name": "my-first-module"
}
```
对!你没看错,只需要定义一个 `name` 字段,即可声明这是一个模块包!
### 配置文件参数
#### - description
- 类型:`string`
- 含义:模块的描述。
??? note "点我查看编写实例:"
```json
{
"name": "my-first-module",
"description": "这个是一个示例模块打包教程"
}
```
#### - version
- 类型string。
- 含义:模块的版本。
版本处理方式和 Composer 基本一致,建议使用三段式,也就是 `大版本.小版本.补丁版本`。关于三段式版本的描述和规范,见 [到底三段式版本号是什么?](https://www.chrisyue.com/what-the-hell-are-semver-and-the-difference-between-composer-version-control-sign-tilde-and-caret.html)。
??? note "点我查看编写实例:"
```json
{
"name": "my-first-module",
"description": "这个是一个示例模块打包教程",
"version": "1.0.0"
}
```
#### - depends
- 类型map of string例如 `{"foo":"bar","baz":"zoo"}`)。
- 含义:模块的依赖关系和版本依赖声明。
此处用作模块的依赖检测,假设模块 `foo` 依赖模块 `bar` 的 1.x 版本但是不兼容 `bar` 的 2.x 版本,可以像 Composer 的 `require` 一样编写版本依赖:`^1.0`。也可以使用 `~`、`>=`、`*` 这些与 Composer 包管理相同逻辑的版本依赖关系,详见 [Composer - 包版本](https://docs.phpcomposer.com/01-basic-usage.html#Package-Versions)。
??? note "点我查看编写实例:"
```json
{
"name": "foo",
"description": "这个是一个示例模块打包教程",
"depends": {
"bar": "^1.0",
"bsr": "*"
}
}
```
#### - light-cache-store
- 类型array of string例如 `["foo","bar"]`)。
- 含义:打包模块时要储存的持久化 LightCache 键名列表。
这里需要配合 LightCache 使用,如果你有一些需要全局缓存的数据,例如动态配置项,比如群服务状态列表,可以先使用 LightCache 存储并使用 `addPersistence()` 持久化,此后在使用模块打包时编写此配置项。
我们假设在项目模块中使用到了 `group-status` 这一个 LightCache那么只需要写 `light-cache-store` 配置项,在模块打包时就会将持久化的数据也打包到 phar 模块包内。
??? note "点我查看编写实例:"
```json
{
"name": "foo",
"description": "这个是一个示例模块打包教程",
"light-cache-store": [
"group-status"
]
}
```
#### - global-config-override
- 类型string | false。
- 含义:解包时是否需要手动编辑全局配置(`global.php`)。
这里如果写 string 类型的,那么就是相当于在解包时会提示此处的内容,内容推荐填写要求解包模块用户需要编辑的项目,比如 「请将 static_file_server 的 status 改为 true以便使用静态文本功能」。
如果是 false那么和不指定此参数效果是一样的无需用户修改 global.php。
??? note "点我查看编写实例:"
```json
{
"name": "foo",
"description": "这个是一个示例模块打包教程",
"global-config-override": "请将 static_file_server 的 status 改为 true"
}
```
#### - allow-hotload
- 类型bool。
- 含义是否允许用户无需解压直接加载模块包文件phar
当此项为 true 时,可以将模块包直接放入 `zm_data/modules` 文件夹下,然后将 `global.php` 中的 `module_loader` 项中的 `enable_hotload` 改为 true启动框架即可加载。
??? note "点我查看编写实例:"
```json
{
"name": "foo",
"description": "这个是一个示例模块打包教程",
"allow-hotload": true
}
```
!!! warning "注意"
如果使用允许热加载,那么模块包中的配置最好不要有 `global-config-override` 和 `light-cache-store`,以此来达到最正确的效果,一般热加载更适合 Library类型的模块。
#### - zm-data-store
- 类型array of string例如 `["foo","bar"]`)。
- 含义:打包时要添加到模块包内的 `zm_data` 目录下的子目录或文件。
其中项目必须是相对路径,不能是绝对路径,且必须是在配置项 `zm_data` 指定的目录(默认会在框架项目的根目录下的 `zm_data/` 目录。
我们假设要打包一个 `{zm_data 目录}/config/` 目录及其目录下的文件,和一个 `main.png` 文件,下方是实例。
??? note "点我查看编写实例:"
```json
{
"name": "foo",
"description": "这个是一个示例模块打包教程",
"zm-data-store": [
"config/",
"main.png"
]
}
```
在打包时框架会自动添加这些文件到 phar 插件包内,到解包时,会自动将这些文件释放到对应框架的 `zm_data` 目录下。
## 打包模块命令
编写配置文件 `zm.json` 后,就可以被框架正常识别为模块形式,你也可以使用对无需打包的模块进行配置以进行分类管理。
### module:list
使用 list 命令可以列出炸毛框架检测到配置文件或打包好的模块。
```
$ ./zhamao module:list
[foo]
类型: source
版本: 1.0.0
描述: 示例模块打包文件
目录: src/Module/Example
没有发现已打包且装载的模块!
```
其中 `[ ]` 内为识别出来的模块名称,由上方用户编写的 `zm.json` 定义,类型为 `source` 是源码形式,也就是指定了 `zm.json` 形式的模块,目录为模块所在子目录。
我们假设打包上方定义的 `foo` 模块,使用下方命令 `module:pack` 即可。
### module:pack
使用 pack 命令可以将配置好的模块打包为 `xxx.phar` 文件并转移或发布给他人。
我们假设打包模块脚手架的默认模块 `src/Module/Example` 下面的模块源码和附带一个 `zm_data` 目录下的文件(我们就随便打包一下 Swoole 的输出日志吧)。`zm.json` 文件内容如下:
```json
{
"name": "foo",
"description": "示例模块打包文件",
"version": "1.0.0",
"allow-hotload": true,
"zm-data-store": [
"crash/swoole_error.log"
]
}
```
然后输入命令:
```
$ ./zhamao module:pack foo
[15:07:11] [I] 模块输出文件:/root/zhamao-framework/zm_data/output/foo_1.0.0.phar
[15:07:11] [S] 打包完成!
```
如果提示文件夹不存在,请先手动创建文件夹:`mkdir /path/to/your/zm_data/output`
在打包后,你将获得一个 `foo_1.0.0.phar` 的文件。
> 如果你没有在 `zm.json` 中指定 `version`,那么输出的 phar 文件是不会带版本号的。
打包后的 phar 内将包含:
- Hello.php
- zm.json
- crash/swoole_error.log
- 必要的框架热加载以及解包需要的配置信息
## 打包命令
```bash
# ./zhamao 和原先的 vendor/bin/start 是完全一致的
./zhamao module:pack <module-name>
```
例如,打包上面的名叫 foo 的模块:`./zhamao module:pack foo`。
打包命令执行后,将会在 `zm_data` 下的 `output` 目录输出一个 phar 文件。如果你指定了 `version` 参数,那么文件名将会是 `${name}_${version}.phar`,如果没有指定版本,那么只会有 `${name}.phar`,同时如果文件已经存在,将覆盖写入。
## 查看模块信息命令
```bash
./zhamao module:list
```
通过此命令可以查看模块相关的信息,如未打包但已配置的模块信息等。

View File

@@ -0,0 +1,84 @@
# 模块解包
从 2.5 版本起,炸毛框架的模块源码支持了打包和分发,分发后必不可少的一步就是将其解包。
解包过程大致为:
1. 检查模块的配置文件是否正常。
2. 检查模块的依赖问题,如果有依赖但未安装,则抛出异常。
3. 检查 LightCache 轻量缓存是否需要写入。
4. 检查 `zm_data` 是否有需要存入的数据。
5. 合并 `composer.json` 文件。
6. 拷贝 `zm_data` 相关的文件。
7. 写入 LightCache 相关数据。
8. 提示用户手动合并 `global.php` 全局配置文件。
9. 拷贝模块 PHP 源文件。
## 解包命令
```bash
./zhamao module:unpack <module-name>
```
首先将待解包的 phar 文件放入 `zm_data` 目录下的 `modules` 文件夹(如果不存在需要手动创建),如果你手动修改过 `global.php` 下面的 `module_loader.load_path` 项,需要放入对应的目录。
放入后,结构如下:
```
zm_data/
zm_data/modules/
zm_data/modules/foo.phar
```
接下来需要知道模块的名称。当然一般情况下phar 的名称可以获取到模块的实际名称,如 `foo`,但最好用 `./zhamao module:list` 列出模块的信息来获取真实的模块名称。
```
./zhamao module:list
# 下面是输出
[foo]
类型: 模块包(phar)
位置: zm_data/modules/我是假的名字.phar
```
解包过程十分简单,只需要执行一次命令即可。
```
./zhamao module:unpack foo
# 下面是输出
[10:05:40] [I] Releasing source file: src/Module/Example/Hello.php
[10:05:40] [I] Releasing source file: src/Module/Example/zm.json
[10:05:40] [S] 解压完成!
```
### 命令参数
在解包时会遇到各种复杂的情况,如源码文件已存在、数据已存在、依赖问题等,通过增加参数可以控制解包时的行为。
#### --overwrite-light-cache
含义:覆盖现有的 LightCache 键值(如果存在的话)。
#### --overwrite-zm-data
含义:覆盖现有的 `zm_data` 下的文件(如果存在的话)。
#### --overwrite-source
含义:覆盖现有的 PHP 模块源文件(如果存在的话)。
#### --ignore-depends
含义:解包时忽略检查依赖。
### 常见问题
如果你解包的模块包要求修改 `global.php`,则会出现类似这样的提示:
```
# ./zhamao module:unpack foo
[14:47:39] [W] 模块作者要求用户手动修改 global.php 配置文件中的项目:
[14:47:39] [W] *请把全局配置文件的light_cache项目中max_strlen选项调整为至少65536
请输入修改模式y(使用vim修改)/e(自行使用其他编辑器修改后确认)/N(默认暂不修改)[y/e/N]
```
一般这种情况,根据第二条提示(第二条提示为打包时填入的 `global-config-override`)。如果输入 y则会自动执行命令 `vim config/global.php`,如果输入的是 e则会等待你手动修改完成文件最后按回车完成修改。默认情况直接回车的话会跳过此步骤如果模块要求了修改但跳过修改安装后可能会有功能缺失等问题。

View File

@@ -8,6 +8,10 @@ DataProvider 是框架内提供的一个简易的文件管理类。
`working_dir()`
## DataProvider::getSourceRootDir()
获取用户的源码根目录,除 Phar 模式外与 `getWorkingDir()` 相同。
## DataProvider::getFrameworkLink()
`ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
@@ -16,6 +20,16 @@ DataProvider 是框架内提供的一个简易的文件管理类。
获取配置项 `zm_data` 指定的目录。
如果指定参数 `$second`,则返回二级目录地址,如果二级目录不存在则自动创建。
```php
DataProvider::getDataFolder("TestModule"); // 例如返回 /root/zhamao-framework/zm_data/TestModule/
```
## DataProvider::getFrameworkRootDir()
返回框架本体的根目录。
## DataProvider::saveToJson()
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
@@ -36,6 +50,45 @@ DataProvider 是框架内提供的一个简易的文件管理类。
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
## DataProvider::scanDirFiles()
递归或非递归扫描目录,返回相对目录的文件列表或绝对目录的文件列表。(非常好用)
定义:`scanDirFiles($dir, $recursive = true, $relative = false)`
`$dir` 为要扫描的目录,`$recursive` 为是否递归,`$relative` 为是否返回相对目录的文件列表。
从给定的目录下开始遍历整个目录,如果将 `$recursive` 设置为 `true`,则会递归扫描子目录,否则将返回包含目录的文件列表。
如果将 `$relative` 设置为 `true`,则会返回文件列表的相对路径,否则返回绝对路径。
例如:假设目录 `/home/test/` 下有两个文件:`test1.txt``testdir/test2.txt`:如果将 `$recursive` 设置为 `true``$relative` 设置为 `false`,则返回的文件列表为:
```json
[
"/home/test/test1.txt",
"home/test/testdir/test2.txt"
]
```
相同条件下,如果将 `$relative` 设置为 `true`
```json
[
"test1.txt",
"testdir/test2.txt"
]
```
如果再把 `$recursive` 设置为 `false`
```json
[
"test1.txt",
"testdir"
]
```
## 其他文件读取
框架比较贴近原生的 PHP所以推荐直接使用原生的方法来读写文件`file_get_contents``file_put_contents`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。

View File

@@ -118,16 +118,16 @@ dump(LightCache::getExpire("test")); // 返回 10
### LightCache::getExpireTS()
获取存储项要过期的时间戳。
获取存储项要过期的时间戳。2.4.3 起可用)
定义:`LightCache::getExpireTS(string $key)`
```php
$s = LightCache::set("test", "hello", 20); //假设这条代码执行时时间戳是 1616838482
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 1616838502
dump(LightCache::getExpireTS("test")); // 返回 1616838502
zm_sleep(10);
dump(LightCache::getExpire("test")); // 返回 null
dump(LightCache::getExpireTS("test")); // 返回 null
```
### LightCache::getMemoryUsage()
@@ -135,7 +135,7 @@ dump(LightCache::getExpire("test")); // 返回 null
获取轻量缓存使用的总空间大小(字节)
```php
LightCache::getMemoryUsage());
LightCache::getMemoryUsage();
```
轻量缓存的内存手工计算方式:(Table 结构体长度` + `KEY 长度 64 字节 + `$size`) * (1 + `$conflict_proportion`) * 列尺寸。

View File

@@ -1,4 +1,8 @@
# MySQL 数据库
# MySQL 数据库(旧版组件)
!!! warning "注意"
此 MySQL 组件为旧版 MySQL 查询器组件,为了统一和提升对未来独立组件的兼容性,现转变为使用 `doctrine/dbal``doctrine/orm` 库来实现查询器,请转到 [MySQL 查询器]()。
## 配置

View File

@@ -0,0 +1,184 @@
# 执行 SQL 语句
在一开始,无论你做什么数据库操作,均需要获取一个 `\ZM\MySQL\MySQLWrapper` 作为你的操作对象。
```php
/** @var \ZM\MySQL\MySQLWrapper $wrapper */
$wrapper = \ZM\MySQL\MySQLManager::getWrapper();
```
!!! tip "提示"
这部分内容部分直接取自 [DBAL - Data Retrieval And Manipulation](https://www.doctrine-project.org/projects/doctrine-dbal/en/2.13/reference/data-retrieval-and-manipulation.html) 原文并直接翻译,如有实际不同请提交 Issue 反馈。
## 执行预处理 SQL 语句
预处理查询很巧妙地解决了 SQL 注入问题,并且可以方便地绑定参数进行查询。
预处理一般是指使用 `?` 占位符或 `:xxx` 命名标签进行参数留空,先处理 SQL 语句再填入数据。
一般 `?` 具有前后位置性,例如如下的查询:
```php
$sql = "SELECT * FROM users WHERE id = ? AND username = ?";
$stmt = $wrapper->getConnection()->prepare($sql);
$stmt->bindValue(1, "1");
$stmt->bindValue(2, "jack");
$resultSet = $stmt->executeQuery();
```
其中 `$resultSet``Statement` 方法相似,此处的对象可能是 [数据库语句对象](../mysql-statement) 或 数据库结果对象(结果对象与语句对象的 `fetchXXX()` 部分一致)。
这里也可以使用命名标签,使用标签可以给相同参数处使用同一个标签:
```php
$sql = "SELECT * FROM users WHERE gender = :name OR username = :name";
$stmt = $wrapper->getConnection()->prepare($sql);
$stmt->bindValue("name", "jack");
$resultSet = $stmt->executeQuery();
```
## 执行常规语句
执行常规语句为 `statement` 方式执行,此方法执行后只返回影响的行数,而不返回结果,适用于 `UPDATE` 等语句。
```php
<?php
$count = $wrapper->executeStatement('UPDATE users SET username = ? WHERE id = ?', array('jwage', 1));
echo $count; // 1
```
## 执行查询语句
为给定的 SQL 创建一个准备好的语句并将参数传递给 executeQuery 方法,然后返回结果集。此方法为上述的「预处理查询语句」的简化版,可直接在第二个参数使用 array 插入绑定参数执行。
```php
$resultSet = $wrapper->executeQuery('SELECT * FROM user WHERE username = ?', array('jack'));
$user = $resultSet->fetchAssociative();
/* $user 值
array(
0 => array(
'id' => 1,
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
)
*/
```
### fetchAllAssociative()
执行查询并将所有结果返回一个数组中。
因此,上面的查询语句还可以直接被简化为一次方法调用:
```php
$resultSet = $wrapper->fetchAllAssociative('SELECT * FROM user WHERE username = ?', array('jack'));
// 结果同 executeQuery()->fetchAllAssociative() 中 $user 的值。
```
### fetchAllKeyValue()
执行查询并将前两列分别作为键和值提取到关联数组中。
```php
$resultSet = $wrapper->fetchAllKeyValue('SELECT username, gender FROM user WHERE username = ?', array('jack'));
/* $resultSet 值
array(
'jack' => 'man'
)
*/
```
### fetchAllAssociativeIndexed()
执行查询并将数据作为关联数组获取,其中键代表第一列,值是其余列及其值的关联数组。
```php
$users = $wrapper->fetchAllAssociativeIndexed('SELECT id, username, gender FROM users');
/*
array(
1 => array(
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
)
*/
```
### fetchNumeric()
查询并返回第一行数据,形式以数字索引方式返回每一列。
```php
$user = $wrapper->fetchNumeric('SELECT * FROM users WHERE username = ?', array('jack'));
/*
array(
0 => 'jwage',
1 => 'man',
2 => '2021-10-12'
)
*/
```
### fetchOne()
仅返回查询结果的第一行第一列的值。
```php
$username = $wrapper->fetchOne('SELECT username FROM users WHERE id = ?', array(1));
echo $username; // jack
```
### fetchAssociative()
返回结果内第一行的关联数组形式的数据。
```php
$users = $wrapper->fetchAssociative('SELECT * FROM users');
/*
array(
'id' => 1,
'username' => 'jack',
'gender' => 'man',
'update_time' => '2021-10-12'
)
*/
```
### delete()
删除查询操作,第一个参数为表名,第二个参数为 `['列名' => '列值']`
```php
<?php
$wrapper->delete('users', array('username' => 'jack'));
// 等同于执行DELETE FROM user WHERE username = ? ,参数列表为('jack')
```
### insert()
插入数据库一行,第一个参数为表名,第二个参数为对应数据。
```php
$wrapper->insert('users', array('id' => 0, 'username' => 'jwage', 'gender' => 'woman', 'update_time' => '2021-10-17'));
// INSERT INTO user (id, username, gender, update_time) VALUES (?,?,?,?) (0,jwage,woman,2021-10-17)
```
### update()
更新数据库,使用给定数据更新匹配键值标识符的所有行。
```php
<?php
$wrapper->update('user', array('username' => 'jwage'), array('id' => 1));
// UPDATE user (username) VALUES (?) WHERE id = ? (jwage, 1)
```

View File

@@ -0,0 +1,7 @@
# 配置
炸毛框架的数据库组件支持原生 SQL、查询构造器去掉了复杂的对象模型关联同时默认为数据库连接池使开发变得简单。
数据库的配置位于 `config/global.php` 文件的 `mysql_config` 段,见 [全局配置](../../../../guide/basic-config#mysql_config)。
如果 `mysql_config.host` 字段为空,则不创建数据库连接池,填写后将创建,且默认保持长连接。

View File

@@ -0,0 +1 @@
你好啊,这里是 Statement。

View File

@@ -0,0 +1,14 @@
# MySQL 数据库简介
炸毛框架的数据库组件对接了 MySQL 连接池,在使用过程中无需配置即可实现 MySQL 查询,同时拥有高并发。
目前 2.5 版本后炸毛框架底层采用了 `doctrine/dbal` 组件,可以方便地构建 SQL 语句。
本章大体查询内容均以下表 `users` 为基础:
| id | username | gender | update_time |
| -- | -------- | ------ | ----------- |
| 1 | jack | man | 2021-10-12 |
| 2 | rose | woman | 2021-10-11 |
#

View File

@@ -2,6 +2,10 @@
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
!!! tip "提示"
框架单进程运行的模式下不需要任何自旋锁。
## 配置
自旋锁使用无需配置,和 LightCache 同源。

View File

@@ -281,6 +281,7 @@
| 参数名称 | 参数范围 | 默认 |
| ----------- | ------------------------------ | ---- |
| command | `string`**必填**,命令字符串 | |
| alias | `string`,可选,命令的别名 | |
| description | `string`,要显示的帮助文本 | 空 |
## 示例1机器人连接框架后输出信息

View File

@@ -165,3 +165,31 @@ public function onThrowing(?Exception $e) {
`ctx()` 为获取当前协程空间绑定的 `request``response` 对象。
## 中间件加载错误处理策略
中间件在某些情况下可能会产生普通 PHP 异常以外的异常,不能被框架的正常错误流程捕获,所以这里额外说明了中间件异常处理的几种策略。
中间件异常处理策略可以在 2.5.2 版本之后通过 `global.php` 中的 `runtime``middleware_error_policy` 设置。
- **0**: 无论被运行事件中间件是否存在,都不抛出异常,继续执行事件。
- **1**: 在框架启动时如果某事件被注解了一个不存在的中间件,则不抛出异常,在执行期间才检测是否存在此中间件,并抛出异常。
- **2**: 严格的中间件检查,在框架启动时就检测所有被注解了中间件的注解事件。
假设我们有一个路由注解 `@RequestMapping("/test")`,同时注解了一个不存在的中间件,如下:
```php
/**
* @RequestMapping("/test")
* @Middleware("foo")
*/
public function testRoute() {
return "I am testing middleware";
}
```
配置项为 0此中间件类不存在的话则会报告 warning并直接执行此函数。
配置项为 1在访问此路由执行此函数时会抛出异常中断此次事件。
配置项为 2在框架启动时抛出致命异常。

View File

@@ -333,6 +333,8 @@ TODO先放着有时间再更。
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
使用 `ctx()->waitMessage()` 时等待用户输入下一条消息功能和 CQBefore 配合过滤消息时需注意,见 [FAQ - CQBefore 过滤不了 waitMessage](/FAQ/wait-message-cqbefore/)
## CQAfter()

7
docs/faq/FAQ.md Normal file
View File

@@ -0,0 +1,7 @@
# FAQ
这里会写一些常见的疑难解答,点击左侧问题名称打开对应解决方法。
如果框架运行过程中发现带有错误码(如 `E00034` 的形式),可以到 [错误码](/guide/errcode) 查看。
框架的常见问题见 [常见问题汇总](/faq/usual-question)。

View File

@@ -0,0 +1,19 @@
# 启动时报错 Address already in use
1. 检查是否开启了两次框架,每个端口只能开启一个框架。
2. 如果是之前已经在 20001 端口或者你设置了别的应用同样占用此端口,更换配置文件 `global.php` 中的 port 即可。
3. 如果是之前框架成功启动,但是使用 Ctrl+C 停止后再次启动导致的报错,请根据下面的步骤来检查是否存在僵尸进程。
- 如果系统内装有 `htop`,可以直接在 `htop` 中开启 Tree 模式并使用 filter 过滤 php检查残留的框架进程。
- 如果系统没有 `htop`,使用 `ps aux | grep vendor/bin/start | grep -v grep` 如果存在进程,请使用以下命令尝试杀掉:
```bash
# 如果确定框架的数据都已保存且没有需要保存的缓存数据,直接杀掉 SIGKILL 即可,输入下面这条
ps aux | grep vendor/bin/start | grep -v grep | awk '{print $2}' | xargs kill -9
# 如果不确定框架是不是还继续运行,想尝试正常关闭(走一遍储存保存数据的事件),使用下面这条
# 首先使用 'ps aux | grep vendor/bin/start | grep -v grep' 找到进程中第二列最小的pid
# 然后使用下面的这条命令假设最小的pid是23643
kill -INT 23643
# 如果使用 ps aux 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死
```

View File

@@ -0,0 +1,22 @@
# 出现 deadlock 字样
一般情况下,如果误操作框架可能会报如下图的错误:
```
===================================================================
[FATAL ERROR]: all coroutines (count: 1) are asleep - deadlock!
===================================================================
[Coroutine-1]
--------------------------------------------------------------------
#0 Swoole\Coroutine\System::sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/global_functions.php:232]
#1 zm_sleep() called at [/Users/jerry/project/git-project/zhamao-framework/src/Module/Example/Hello.php:38]
#2 Module\Example\Hello->onStart() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:205]
#3 ZM\Event\EventDispatcher->dispatchEvent() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/EventDispatcher.php:89]
#4 ZM\Event\EventDispatcher->dispatchEvents() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Event/SwooleEvent/OnWorkerStart.php:130]
#5 ZM\Event\SwooleEvent\OnWorkerStart->onCall() called at [/Users/jerry/project/git-project/zhamao-framework/src/ZM/Framework.php:336]
```
这种错误的出现原因一般是因为协程未结束而 Worker 进程提前退出导致的,这个错误也可手动造成(在任意 Worker 进程内的位置使用 `zm_yield()` 且不使用 `zm_resume()` 恢复,期间使用 reload 或 stop 重启或停止框架就会报错)。
还有一种情况是数据库、文件读取或下载上传还没有传送结束,时间已经超时,在关闭或重启框架时不得不强行切断协程的运行。这种情况建议根据下方的打印输出栈进行插错,建议将协程运行时间长的过程缩短或调长 `swoole` 配置项下面的 `max_wait_time` 时间2.4.3 版本起此参数默认为 5 秒。

View File

@@ -0,0 +1,5 @@
# 使用 LightCache 关闭时无法正常保存持久化
LightCache 因为是跨内存使用的,所以每次重启和关闭框架时,都只会让其中一个进程去保存。因为在 2.4.2 版本开始,持久化的逻辑发生了更改,不再支持 `expire = -2` 进行设置持久化(因为那样会很容易让开发者写错),仅支持使用 `LightCache::addPersistence($key)` 这样的方式进行设置持久化,所以在 2.4.2 版本以后,请使用此方法进行持久化设置,保证数据不丢失。
此外2.4.2 版本起,不再支持用户手动调用 `savePersistence()` 方法,普通用户不可手动调用此方法,否则会导致数据出错。

View File

@@ -0,0 +1,66 @@
# 框架常见问题(持续更新)
## 如何正确地强制退出炸毛框架?
首先要知道一个概念,炸毛框架和传统的 PHP 以及其他如 Python 等语言的轻量框架都不同,框架启动后会依次启动 Master、Manager、Worker 等多个进程,而用户启动时入口的 PHP 进程就是 Master 进程,在一些对框架的正常中止、热重启上,我们给 Master 进程发送相应的 Linux 信号(如 SIGTERM即可对整个框架的多个进程生效无需给每个进程发送。
但是如果因为用户的误操作,导致炸毛框架其中的一个或多个进程阻塞,或者比如将框架挂在 screen 等守护但是守护服务进程被杀掉,总之就是无法使用 Ctrl+C 的方式正常关闭框架,这时就需要正确地杀掉所有框架进程(这固然可能会造成内存的缓存数据丢失)。
### v2.7.0 及以上版本教程
- 安全关框架指令:`./zhamao server:stop`
- 万能杀死所有框架进程指令:`./zhamao server:stop --force`
- 监视框架是否在运行:`./zhamao server:status`
- Worker 进程卡死:连续按 5 次 Ctrl+C 即可强行杀掉所有进程SIGKILL
### v2.6.6 及以下版本教程
!!! warning "注意"
下方涉及 `ps` 命令后使用 `grep` 过滤的框架进程方式,如果你的服务器同时有其他使用 PHP 启动的服务,命令行刚好有 `server` 字样,可能会导致误杀,如果有影响的话,建议将 `grep server` 换成你启动时命令行的特殊参数或手动排除!
**一、**首先,使用 `ps``htop``netstat -nlp` 等命令确定框架的入口进程(也就是 Master 进程的 pid
确认方式示例如下:
- 如果你使用的是 >=2.4 版本的框架,在框架启动时就会在最先开始的 motd 上方显示 `master_pid`,如果你还能找到此处的显示,那么恭喜你,可以直接进行下面的第二步。
- 如果你不能正常通过框架的方式找到 pid可以通过命令 `ps aux | grep php | grep server` 的方式找到框架所有的进程。其中列出的相关框架的进程,可以寻找 pid 最小的进程,即为 Master 进程。关于如何区分进程对应关系,见本页 [使用 Linux 工具辨别框架进程]()。
- 如果你对 `ps` 不熟悉,可以使用 `htop` 工具,使用 `F5 Tree` 方式显示,并且使用 `F4` 的 Filter过滤 `php``bin/start` 等字样,找到进程树。
**二、**然后,确定框架是否正常运行且正常流程关闭。
如果框架能正常运行,比如可以通过访问浏览器的 `http://地址:端口/httpTimer` 等 HTTP 路由,可以使用 `SIGINT``SIGTERM` 信号正常关闭框架。我们假设 Master 进程的 pid 为 31234`kill -TERM 31234``kill -INT 31234`,如果稍后使用 `ps aux | grep php | grep server` 命令发现没有进程存在(排除掉 grep 自身的进程),说明可以正常关闭,此关闭方法为正常停止流程,即保存了 `LightCache` 等内存缓存持久化的数据。
如果以上方式没有任何效果,继续看第三步。
**三、**不能正常流程关闭,需要手动杀掉所有进程。
首先使用 `ps aux | grep php | grep server | grep -v grep | awk '{print $2}'` 列出框架所有进程的 pid确认无误后在此条命令后接 `| xargs kill -9` 即可:
```bash
# 列出进程只显示包含php只显示包含server排除grep本身进程显示第二列的pid使用xargs循环kill这里面的进程
ps aux | grep php | grep server | grep -v grep | awk '{print $2}' | xargs kill -9
```
## 如何使用 Linux 工具查看框架进程状态?
框架有多个进程,有时候我们需要通过监视进程状态来确定框架是否正常运行或查看框架的资源占用率。首先一个大概念,老生常谈,炸毛框架由 Master、Manager、Worker、TaskWorker进程组成的。
如果使用 htop 工具,就比较简单,比如我启动了一个应用,使用炸毛框架编写的垃圾分类小程序 API 服务器,在 htop 命令后找到如图这部分(下面的树状图是按 F5 后切换为树状显示,避免进程刷太快可以输入 `Shift+z`
![image-20210708003903652](https://static.zhamao.me/images/docs/image-20210708003903652.png)
其中,`-zsh` 下有唯一一个 php 进程,在图中对应的第一列 pid 为 `16258`,代表 Master 进程。
Master 进程下的唯一一个子进程(白色的是进程,绿色是线程),在图中对应的 pid 为 `16263`,代表 Manager 进程,用作管理 Worker 进程。
Manager 进程下的子进程,连号部分为对应的 Worker 进程,比如图中的 `16266``16267``16268``16269` 分别代表 `Worker #0``Worker #1``Worker #2``Worker #3` 四个 Worker 进程。
如果你还设置了 TaskWorker 进程TaskWorker 进程的 pid 会和 Worker 进程一样是连续的,一般会接在 Worker 进程后面。
`htop` 使用方向键选择进程,选择到对应进程后可以使用 `F9` 来选择 kill 指令,比如让框架热重启,可以将光标移到 Master 进程上,使用 `SIGUSR1`
![image-20210708004921655](https://static.zhamao.me/images/docs/image-20210708004921655.png)

View File

@@ -0,0 +1,23 @@
# CQBefore 过滤不了 waitMessage
因为 `waitMessage()` 功能是要等待接收下一个消息事件的,而消息事件又会被 CQBefore 走一遍。但是这里就会有一个问题,那 `waitMessage()` 的消息会不会走 CQBefore 呢?(显然不会啊!这个问题的题目就是这个!)
框架在 2.4.2 版本之前是无法过滤 waitMessage() 的(之前在 2.1 版本左右的几个版本是可以的,但这里不讨论历史版本),从 2.4.2 版本起支持过滤 `waitMessage`,但是需要设置一下 `@CQBefore` 的级别。
```php
/**
* @CQBefore("message",level=201)
*/
public function filter1() {
return true;
}
/**
* @CQBefore("message")
*/
public function filter2() {
return true;
}
```
如果 `level >= 200`,那么此注解事件则会过滤 `waitMessage()`,如果 `level < 200`,则不会。(`@CQBefore` 的默认 level 为 20所以默认情况下是不会过滤 waitMessage 的)

View File

@@ -20,9 +20,10 @@
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `config_dir` | 存放 saveToJson() 方法保存的数据的目录 | `zm_data` 下的 `config/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `runtime` | 一些框架运行时调整的设置 | 见子表 `runtime` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `sql_config` | MySQL 数据库连接信息 | 见子表 `sql_config` |
| `mysql_config` | MySQL 数据库连接信息 | 见子表 `mysql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
@@ -34,6 +35,7 @@
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表,在 Swoole 服务器启动前就被加载 | 空 |
| `onebot` | OneBot 协议相关配置 | 见子表 `onebot` |
| `remote_terminal` | 远程终端相关配置 | 见子表 `remote_terminal` |
| `module_loader` | 模块/插件 加载相关配置 | 见子表 `module_loader` |
### 子表 **swoole**
@@ -47,6 +49,17 @@
| `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) |
| `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` |
### 子表 runtime
| 配置名称 | 说明 | 默认值 |
| ----------------------------- | ------------------------------------------------------------ | --------------------------------------- |
| `swoole_coroutine_hook_flags` | Swoole 启动时一键协程化 Hook 的 Flag 值,详见 [一键协程化](http://wiki.swoole.com/#/runtime?id=%e5%87%bd%e6%95%b0%e5%8e%9f%e5%9e%8b) | `SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL)` |
| `swoole_server_mode` | Swoole Server 启动的进程模式,有 `SWOOLE_PROCESS``SWOOLE_BASE` 两种,见 [启动方式](http://wiki.swoole.com/#/learn?id=swoole_process) | `SWOOLE_PROCESS` |
| `middleware_error_policy` | 中间件错误处理策略,见 [中间件 - 错误处理策略](../../event/middleware/#_6) | 1 |
| `reload_delay_time` | 框架 reload 重载命令接收后延迟的时间毫秒0 为不等待) | 800 |
| `global_middleware_binding` | 给注解事件绑定全局中间件,见 [中间件 - 全局中间件](../../event/middleware/#_6) | `[]` |
| `save_console_log_file` | 当这里输入字符串路径时,所有 `Console::xxx()` 输出的日志都将保存到目标文件 | false |
### 子表 **light_cache**
| 配置名称 | 说明 | 默认值 |
@@ -63,17 +76,18 @@
| -------- | --------------------------- | ------ |
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
### 子表 **sql_config**
### 子表 **mysql_config**
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------ | ------------------------------------------------------------ |
| `sql_host` | 数据库地址(留空则不使用数据库) | 空 |
| `sql_port` | 数据库端口 | 3306 |
| `sql_username` | 连接数据库的用户名 | |
| `sql_database` | 要连接的数据库名 | |
| `sql_password` | 数据库连接密码 | |
| `sql_options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_EMULATE_PREPARES => false]` |
| `sql_default_fetch_mode` | PDO 的 fetch 模式 | `PDO::FETCH_ASSOC` |
| `host` | 数据库地址(留空则不使用数据库) | 空 |
| `port` | 数据库端口 | 3306 |
| `username` | 连接数据库的用户名 | |
| `dbname` | 要连接的数据库名 | |
| `password` | 数据库连接密码 | |
| `options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]` |
| `pool_size` | MySQL 连接池大小 | 64 |
| `charset` | MySQL 字符集 | `utf8mb4` |
### 子表 **redis_config**
@@ -95,11 +109,13 @@
### 子表 onebot
| 配置名称 | 说明 | 默认值 |
| ----------------- | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99999 |
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99 |
| `message_convert_string` | 是否将数组格式的消息转换为字符串以保证与旧版本的兼容性 | true |
| `message_command_policy` | CQCommand命令匹配后执行流程`interrupt` 为不执行后续 CQMessage`continue` 为继续 | `interrupt` |
### 子表 remote_terminal
@@ -110,6 +126,13 @@
| `port` | 远程终端监听的 TCP 端口 | 20002 |
| `token` | 远程终端连接的令牌(如果为空("")则不验证) | "" |
### 子表 module_loader
| 配置名称 | 说明 | 默认值 |
| -------- | ------------------------------------------------------------ | ----------- |
| `enable_hotload` | 是否开启热加载模块包的功能 | false |
| `load_path` | 模块包加载的目录地址 | `zm_data` 下的 `modules` |
## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境development / staging / production来编写配置文件。

78
docs/guide/errcode.md Normal file
View File

@@ -0,0 +1,78 @@
| 异常码 | 含义 | 解决方案 |
| ------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| E00001 | 炸毛框架未检测到 PHP 安装了 Swoole 扩展 | 根据文档安装扩展去! |
| E00002 | Swoole 扩展安装的版本过低 | 升级 Swoole 版本,最好为最新版。 |
| E00003 | PHP 版本过低 | 升级 PHP 版本,至少为 7.2。 |
| E00004 | Swoole 版本低于 4.6.7 且未安装 pcntl 扩展 | 安装 pcntl 扩展或升级 Swoole 至少为4.6.7。 |
| E00005 | 在框架命令行解析过程中出现了致命错误 | 请根据提示的错误位置进行调试和修复,如果未解决请将问题反馈给作者。 |
| E00006 | 炸毛框架在源码模式启动时未能修改 composer.json 文件 | 检查源码模式下 composer.json 文件是否正常可写可读。 |
| E00007 | 框架在启动时未找到 global.php 全局配置文件 | 如果是使用 `composer create-project` 或用 git 来克隆 starter 仓库的,需要先使用 `vendor/bin/start init` 指令,再启动服务器。 |
| E00008 | 框架在启动时给用于存储连接数据的共享内存表初始化失败 | 请检查系统内存是否过小,如果一切正常,此问题一般是框架内部导致的问题,请将错误日志反馈给开发者。 |
| E00009 | 使用 `--remote-terminal` 时远程终端处理命令出现异常或致命错误 | 检查自身的远程终端是否正确配置和使用,自定义的 `@TerminalCommand` 注解是否抛出了致命错误。 |
| E00010 | 框架在第一步的启动阶段抛出异常或致命错误,导致框架不能继续运行 | 此错误涵盖的错误内容较多,请根据实际抛出的异常内容进行处理或反馈给开发者。<br />如果你使用了 `@SwooleHandler``@OnSetup` 注解,那么可以自行检查一下注解绑定的函数有没有出错。 |
| E00011 | 框架在调用 Swoole 服务器启动 `$server->start()` 过程中出现了异常 | 此问题未经测试,暂无解决方案,也没有遇到过,如果有发生,请将错误反馈开发者。 |
| E00012 | 框架在启动时调用脚本解析 `@SwooleHandler``@OnSetup` 时出现了异常 | 留个坑下次写TODO。 |
| E00013 | 使用命令行参数动态设置启动的 Worker/TaskWorker 进程数时输入了非法的数字 | 填写合法的数字或不使用此功能。 |
| E00014 | 炸毛框架的启动命令报错,提示没有找到 PHP 环境 | 使用 `./install-runtime.sh` 命令安装便携的静态 PHP 环境或根据教程和 Linux 发行版安装环境。 |
| E00015 | 启动命令启动框架找不到框架本体的入口文件 | 请检查 Composer 拉取的框架代码是否完整。 |
| E00016 | 连接中断后 `@OnCloseEvent` 事件抛出异常 | 检查 `@OnCloseEvent``@OnSwooleEvent("close")` 注解事件。 |
| E00017 | 框架作为 WebSocket 服务器收到客户端数据后 `@OnMessageEvent``@OnSwooleEvent("message")` 或 OneBot 相关事件抛出了未被捕获的异常或错误 | 检查 `@OnSwooleEvent("message")``@OnMessageEvent` 或 OneBot 相关注解事件。 |
| E00018 | 框架设置 `access_token` 参数为自定义闭包函数,有新 WebSocket 连接接入但是闭包函数返回失败 | 说白了就是自定义的 `access_token` 验证失败。如果是自己的 OneBot 客户端连接,那么请检查你的函数或 OneBot 客户端那边和框架约定的 token 是否一致,如果将框架开到了公网并有人尝试连接但失败了说明是正常现象。 |
| E00019 | 框架设置了 `access_token` 为固定字符串,但是 WebSocket 新连接验证 Token 失败 | 如果是自身行为,比如 OneBot 客户端接入,请检查 Token 是否一致。如果不需要设置 Token请检查全局配置文件的 `access_token` 项是否为空字符串。 |
| E00020 | 框架在收到 WebSocket 连接后触发 `@OnOpenEvent` 注解事件过程中抛出了异常 | 检查用户代码中 `@OnOpenEvent``@OnSwooleEvent("open")` 注解事件下的代码是否有问题。 |
| E00021 | 框架在处理 pipeMessage 事件时出现了异常 | 如果写了 `@OnPipeMessageEvent` 注解事件,请检查对应注解事件。如果未设置,可能是框架内部错误,请将报错信息反馈开发者。 |
| E00022 | 调用 `ProcessManager::sendActionToWorker()` 方法时,调用此方法的进程不是 Worker 或 Manager 进程 | 如果你在 Master 进程调用此方法会直接报此错误,框架不支持从 Master 进程调用此方法。 |
| E00023 | 框架在收到 HTTP 请求后处理过程中出现了未捕获的异常 | 检查 HTTP 请求相关的注解解析代码,如果调用栈显示非用户代码所致,请将错误信息反馈开发者。 |
| E00024 | 框架使用 `--watch` 时无法使用热更新并报错 | PHP 未安装 inotify 扩展,请使用 pecl 安装 inotify 扩展并启用后再试。 |
| E00025 | 框架使用终端输入时产生了未捕获的异常或致命错误 | 检查 `@TerminalCommand` 注解事件或检查使用动态命令的内容(例如 bc 或 call 运行的代码或函数有没有错误)。 |
| E00026 | 框架使用 `@OnTask` 注解在 TaskWorker 进程中执行函数抛出了异常 | 检查 TaskWorker 运行的任务代码是否会抛出未捕获的异常。 |
| E00027 | 框架在运行过程中 Worker 进程发生未捕获的异常导致崩溃退出 | 见 [Issue #38](https://github.com/zhamao-robot/zhamao-framework/issues/38)。 |
| E00028 | PHP 未安装 pdo_mysqlmysqlnd+PDO扩展 | 安装 php-mysql以 ubuntu 为例apt install php-pdo php-mysql。 |
| E00029 | PHP 未安装 redis 扩展 | 安装 redis 扩展。 |
| E00030 | 框架在 Worker 进程启动时出现错误 | 检查 `@OnStart` 相关事件的问题,或根据报错信息定位问题所在。此问题可能较常见,一般在启动时导致的。 |
| E00031 | 框架在启动前解析代码出现错误 | 检查模块代码中是否有 PHP 语法错误。 |
| E00032 | 上下文的 class 没有 implements ContextInterface 接口 | 如果从 global.php 设置了自定义上下文类,那么请检查上下文类有没有根据文档标准来编写接口。 |
| E00033 | Worker 进程运行过程使用 `zm_*` 方法过程中抛出了未被捕获的异常 | 一般是由 `zm_go()``zm_timer_tik()` 造成的,协程或计时器内抛出了异常未被捕获。建议根据 trace 检查是什么地方抛出的异常。 |
| E00034 | 由带中间件的 `@OnTick` 计时器产生了未被捕获的异常 | 建议检查计时器内的代码抛出异常位置,如果错误处理也是一部分功能,建议使用 `try catch` 自行捕获。 |
| E00035 | CQ 码相关错误 | 根据提示检查调用 CQ 码的代码即可。 |
| E00036 | OneBot WebSocket API 推送失败,可能是 WebSocket 客户端出现了问题 | 建议检查 OneBot 客户端和框架的连接是否正常。 |
| E00037 | OneBot 机器人端连接未找到,或单例模式连接了多个机器人 | 根据提示信息进行修复,比如机器人 xxx 未连接到框架,就看一下 OneBot 客户端是否启用和配置正常。 |
| E00038 | 图灵机器人 API 调用出错 | 根据提示和图灵错误码进行检查。 |
| E00039 | 使用 build 命令时检测到目标目录不存在 | 重新指定一个存在的目录即可。 |
| E00040 | 使用 build 命令时检测到 PHP 未设置 `phar.readonly=Off` | 修改 php.ini 将此项设置为 Off。 |
| E00041 | 使用 init 命令时未检测到 composer.json 文件 | 检查引用框架的 composer.json 文件位置。 |
| E00042 | 框架使用 init 命令时启动模式不是 Composer 模式 | 如果你是使用 git 且下载的仓库是 `zhamao-robot/zhamao-framework.git`,那么代表其以源码模式启动,详见[框架启动模式 - 炸毛框架 v2 (zhamao.xin)](https://framework.zhamao.xin/advanced/custom-start/)。 |
| E00043 | MySQL 数据库出错,抛出异常 | 根据提示信息检查 MySQL 语句是否正确,数据库是否连接正常等,其他不能解决的问题建议反馈开发者。 |
| E00044 | 打包模块过程中抛出了异常 | 根据提示文本进行修复错误的指令和代码即可。 |
| E00045 | 打包模块过程中无法储存 `light-cache-store` 项指定的缓存数据 | 根据提示进行修复即可。 |
| E00046 | Redis 连接池在使用过程中未提前初始化,可能是未设置全局配置文件启用 Redis 连接池 | 检查 global.php 是否设置 Redis 服务器。 |
| E00047 | Redis 连接池初始化失败 | 根据提示报错信息进行修复,先检查 global.php 是否设置 Redis 服务器。 |
| E00048 | LightCache 未初始化 | LightCache 会根据 global.php 初始化申请内存,如果申请出错请根据启动时的报错信息调整配置。 |
| E00049 | LightCache 不能接收字符串、数组、int 之外的变量数据 | 检查传入的数据类型。 |
| E00050 | 系统内存不足LightCache 申请内存失败 | 让 PHP 可使用的内存或系统内存变大,也可以调小全局配置中设置的 LightCache 配置项。 |
| E00051 | LightCache 的 Hash 冲突过多,导致无法动态空间分配内存 | 设置 `hash_conflict_proportion` 大一些(范围 0-1默认是 0.6)。 |
| E00052 | 在 /src/ 目录下不可以直接标记为模块(zm.json),因为命名空间不能为根空间 | 将模块标记文件 zm.json 放到子目录下,不能直接放在 src 目录下。 |
| E00053 | 框架检测到了重名模块 | 更改模块名称。 |
| E00054 | 打包好的模块文件phar内检测不到 zm.json 原始模块标记文件存在 | 检查 phar 模块是否完整。 |
| E00055 | 打包好的模块文件phar不能正常读取模块标记文件zm.json | 检查 phar 模块是否完整。 |
| E00056 | 未开启 TaskWorker 进程 | 请先修改 global 配置文件启用。 |
| E00057 | 调用 `DataProvider::saveToJson()` 失败,因为传入了多级目录 | `saveToJson()` 方法的 `$filename` 参数只能最多到第二级子目录,不能有三级,例如 `foo/bar`。 |
| E00058 | 调用 `DataProvider::scanDirFiles()` 失败,因为传入的 `$relative` 错误 | `$relative` 参数只能传入 `string/false` 两种类型。 |
| E00059 | 使用 `MessageUtil::downloadCQImage()` 失败,因为指定下载的目录不存在 | 新建目录,检查目录地址是否是绝对路径,如果手动指定了目录,最好为绝对路径。 |
| E00060 | 使用 `MessageUtil::downloadCQImage()` 失败,因为图片下载失败 | 检查下载图片的链接地址是否能正常的访问。 |
| E00061 | 使用 `set_coroutine_params()` 失败,因为不能在非协程环境使用此函数 | 检查调用此函数的位置,注意不能在非协程环境(比如 Master 进程)下调用。 |
| E00062 | 注解事件非法或不可回溯 | 不能在非注解调用的类中的方法调用 `EventTracer` 方法。 |
| E00063 | 模块检测到依赖版本问题 | 检查是否部署或正确配置依赖的模块/插件版本。 |
| E00064 | 模块系统检测到依赖的模块不存在或未安装部署 | 检查依赖的模块是否正确存在于源码目录。 |
| E00065 | 模块系统检测到打包的模块文件中未含有 `light_cache_store.json` 文件 | 可能是打包此模块后打包的文件损坏,请询问原开发者打包一个新的没有损坏的 phar 文件。 |
| E00066 | 模块打包时 `zmdata-store` 指定的文件或目录不存在 | 检查是否存在,检查写的相对路径是否有误(相对路径的初始路径为框架当前的 `zm_data` 配置的目录。 |
| E00067 | 模块解包合并 `composer.json` 时没有找到项目原文件 | 检查项目的工作目录下是否有 `composer.json` 文件存在。 |
| E00068 | 模块解包时无法正常拷贝文件 | 检查文件夹是否正常可以创建和写入。 |
| E00069 | 框架不能启动两个 ConsoleApplication 实例 | 不要重复使用 `new ConsoleApplication()`。 |
| E00070 | 框架找不到 `zm_data` 目录 | 检查配置中指定的 `zm_data` 目录是否存在。 |
| E00071 | 框架找不到对应类型的 API 调用类 | 检查 `getExtendedAPI($name)` 传入的 `$name` 是否正确 |
| E00072 | 上下文无法找到 | 检查上下文环境,如是否处于协程环境中 |
| E00073 | 在类中找不到方法 | 检查调用对象是否存在对应的方法method或检查是否插入了对应的macro宏方法 |
| E00074 | 参数非法 | 检查调用的参数是否正常(此处可能有多处问题,请看具体调用栈炸掉的地方) |
| E99999 | 未知错误 | |

View File

@@ -2,111 +2,49 @@
> 这篇为炸毛框架以及环境的部署教程。
框架部署分为环境部署和框架部署。框架部署非常简单,只需要通用的指令,下方主要说环境部署
框架部署分为两部分,一部分是安装 PHP 环境,另一部分是通过 Composer 或 GitHub 拉取框架的脚手架
## Docker 部署 PHP 环境
如果你不想干扰主机的环境,可以使用 Docker 进行拉取框架适用的 PHP7 with Swoole Extension Docker Container。本框架安装教程中使用的 DockerHub 及 Dockerfile 构建文件所构建的容器均为独立的容器,和框架无关,此 Docker 也可以用作运行**其他基于 php-cli 模式的项目**。
## 一键下载静态 PHP 环境和框架脚手架
方法一、直接拉取远程容器(推荐
```bash
docker pull zmbot/swoole
```
方法二、从 Dockerfile 构建容器
```bash
git clone https://github.com/zhamao-robot/zhamao-swoole-docker.git
cd zhamao-swoole-docker/
docker build -t zm .
```
!!! note "从 Dockerfile 构建容器的提示"
使用 Dockerfile 构建后,需要将下方所有的 `zmbot/swoole` 全部更换成 `zm`,或者你上方指令中的 `-t` 参数后方的名称,具体可以详情查阅 Docker 的文档。
## 主机部署 PHP 环境
### Debian 系列Ubuntu、Kali
需要的系统内软件包为:`php php-dev php-mbstring gcc make openssl php-mbstring php-json php-curl php-mysql wget composer`
下面是一个一键安装的命令行(最小安装,需 root 权限):
从 2.4.4 版本起,炸毛框架支持一键拉取一个静态的 PHP 运行时和脚手架,只需运行下面的脚本即可。(开发环境推荐此方法
```bash
apt-get update && apt-get install -y software-properties-common && 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.7.tar.gz && tar -zxvf v4.5.7.tar.gz && cd swoole-src-4.5.7/ && phpize && ./configure --enable-openssl --enable-mysqlnd && make -j2 && make install && (echo "extension=swoole.so" >> $(php -i | grep "Loaded Configuration File" | awk '{print $5}'))
```
# 将会把 PHP、框架都安装在此目录下
mkdir zhamao-app/ # 这里可以取自己的项目名字
cd zhamao-app/
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
### macOS (with Homebrew)
macOS 系统下的部署相对简单很多,只需要使用 Homebrew 安装以下包和执行安装命令即可
!!! note "给 macOS 开发者的提示"
因为苹果新的 Apple Sillicon 对 Homebrew 的支持目前仅限于 Rosetta2 转译版,
所以在使用 M1-based Mac 时出现问题暂时无解。
使用以下指令可能会遇到报错等问题,如有疑问可直接使用 Docker 或咨询我(炸毛框架开发者)。
```bash
brew install php composer
pecl install swoole
```
### 其他 Linux 发行版
其他 Linux 发行版,如 CentOSFedoraArch 等暂时还没有经过严格的测试需要哪些依赖,大体和 Ubuntu、Debian 系需要的依赖包差不多,可根据安装过程中报错提示依次安装,或者直接使用 Docker 环境。
## 安装框架
恭喜你,前方通过 Docker 或主机安装环境后可以开始构建框架的开发脚手架了!
如果你是通过**主机安装 PHP 部署的环境**,下方是通过脚手架来构建项目的命令行。
```bash
composer create-project zhamao/framework-starter zhamao-app
cd zhamao-app/ # 这个是你可以自己定义的名称
vendor/bin/start server # 启动框架
```
如果是通过 **Docker 部署的环境**,则需要在先克隆脚手架后在文件夹内使用 Docker 命令下的 `composer update`。(如果主机环境有 composer 也可以使用 `composer create-project` 的方式拉取脚手架。)
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole composer update
```
或者在 Docker 环境下,你可以直接使用如下方法拉取和快速启动一个最标准的框架。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter
./run-docker.sh # 在正式版炸毛框架 v2 发布后可用,测试版暂不放出
```
!!! tip "提示"
如果国内 Composer 下载过慢,可以使用阿里云的 Composer 镜像加速。
```bash
# 仅对当前的项目使用阿里云加速
composer config repo.packagist composer https://mirrors.aliyun.com/composer/
# 对全局的 Composer 使用阿里云加速
composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
```
## 启动框架
本地环境启动方式:
```bash
cd zhamao-framework-starter
# 安装完成后的启动框架命令2.5.0 版本后可省略掉 runtime/php 前缀)
vendor/bin/start server
# 扩展用法:使用静态 PHP 版本的 Composer update
runtime/composer update
# 扩展用法:使用静态 PHP 运行别的 CLI 脚本
runtime/php path/to/your/script.php
```
使用 Docker 启动:
> 有关静态 PHP 的多种用法(如 Composer见 [进阶 - PHP 环境高级](/advanced/php-env)
## 使用 Docker 部署 PHP 和框架
你也可以使用 Docker 进行拉取 PHP 环境。
```bash
cd zhamao-framework-starter
# 拉取 Docker 镜像
docker pull zmbot/swoole
# 再通过 GitHub 或其他方式拉取框架脚手架
git clone --depth=1 https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
# Docker 内使用 Composer 更新依赖
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole composer update
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start init
# 使用 Docker 启动框架
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start server
```
启动后你会看到和下方类似的初始化内容,表明启动成功了
```verilog
@@ -114,17 +52,13 @@ $ vendor/bin/start server
host: 0.0.0.0 | port: 20001
log_level: 2 | version: 2.0.0
config: global.php | worker_num: 4
working_dir: /Users/jerry/project/git-project/zhamao-framework
working_dir: /app/zhamao-framework
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
[14:27:31] [I] [#0] Worker #0 启动中
[14:27:31] [I] [#2] Worker #2 启动中
[14:27:31] [I] [#1] Worker #1 启动中
[14:27:31] [I] [#3] Worker #3 启动中
[14:27:31] [S] [#3] Worker #3
[14:27:31] [S] [#0] Worker #0
[14:27:31] [S] [#2] Worker #2
@@ -133,9 +67,11 @@ working_dir: /Users/jerry/project/git-project/zhamao-framework
单纯运行 炸毛框架 后,如果不部署或安装启动任何机器人客户端的话,仅仅相当于启动了一个 监听 20001 端口的WebSoket + HTTP 服务器。你可以通过浏览器访问http://127.0.0.1:20001 ,或者你部署到了服务器后需要输入服务器地址。
!!! note "安装和部署总结"
## 命令总结
根据上方描述,此文档中剩余提到的所有 Bash 命令,如果使用 Docker 部署环境,则需要加上 Docker 环境的指令:`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole`,如执行其他 Linux 指令(以查看 PHP 版本为例):`docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole php -v`
1. 对于框架的启动,必须 cd 到项目的跟目录,比如 `cd zhamao-app/` 进入到项目根目录
2. 无论何种方式启动,启动框架的命令格式都为这个格式:`{php二进制路径} vendor/bin/start server {--如果需要参数的话这样跟}`
3. 第二条的 `php 二进制路径` 指的是,比如使用第一种静态 PHP 环境,这里写 `runtime/php` 就好了,如果是安装到系统的 PHP 的话,这里为空,如果是 Docker 部署的环境,则这里填 `docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole`
## 使用 IDE 等工具开发代码

View File

@@ -30,7 +30,103 @@ OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)
由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`
=== "config.hjson新格式)"
=== "config.yml新格式)"
```yaml hl_lines="2 3 78 83"
account: # 账号相关
uin: 1233456 # QQ账号
password: '' # 密码为空时使用扫码登录
encrypt: false # 是否开启密码加密
status: 0 # 在线状态 请参考 https://github.com/Mrs4s/go-cqhttp/blob/dev/docs/config.md#在线状态
relogin: # 重连设置
delay: 3 # 首次重连延迟, 单位秒
interval: 3 # 重连间隔
max-times: 0 # 最大重连次数, 0为无限制
# 是否使用服务器下发的新地址进行重连
# 注意, 此设置可能导致在海外服务器上连接情况更差
use-sso-address: true
heartbeat:
# 心跳频率, 单位秒
# -1 为关闭心跳
interval: 5
message:
# 上报数据类型
# 可选: string,array
post-format: string
# 是否忽略无效的CQ码, 如果为假将原样发送
ignore-invalid-cqcode: false
# 是否强制分片发送消息
# 分片发送将会带来更快的速度
# 但是兼容性会有些问题
force-fragment: false
# 是否将url分片发送
fix-url: false
# 下载图片等请求网络代理
proxy-rewrite: ''
# 是否上报自身消息
report-self-message: false
# 移除服务端的Reply附带的At
remove-reply-at: false
# 为Reply附加更多信息
extra-reply-data: false
output:
# 日志等级 trace,debug,info,warn,error
log-level: warn
# 是否启用 DEBUG
debug: false # 开启调试模式
# 默认中间件锚点
default-middlewares: &default
# 访问密钥, 强烈推荐在公网的服务器设置
access-token: ''
# 事件过滤器文件目录
filter: ''
# API限速设置
# 该设置为全局生效
# 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
# 目前该限速设置为令牌桶算法, 请参考:
# https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
rate-limit:
enabled: false # 是否启用限速
frequency: 1 # 令牌回复频率, 单位秒
bucket: 1 # 令牌桶大小
database: # 数据库相关设置
leveldb:
# 是否启用内置leveldb数据库
# 启用将会增加10-20MB的内存占用和一定的磁盘空间
# 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable: true
# 连接服务列表
servers:
# 添加方式,同一连接方式可添加多个,具体配置说明请查看文档
#- http: # http 通信
#- ws: # 正向 Websocket
#- ws-reverse: # 反向 Websocket
#- pprof: #性能分析服务器
# Zhamao Framework 所需要的服务器配置
- ws-reverse:
# 是否禁用当前反向WS服务
disabled: false
# 反向WS Universal 地址
# 注意 设置了此项地址后下面两项将会被忽略
universal: ws://127.0.0.1:20001/
# 反向WS API 地址
api: ws://your_websocket_api.server
# 反向WS Event 地址
event: ws://your_websocket_event.server
# 重连间隔 单位毫秒
reconnect-interval: 3000
middlewares:
<<: *default # 引用默认中间件
```
=== "config.hjsonv1.0.0-beta2或更早版本所用格式"
``` json hl_lines="3 5 81 84"
{
@@ -235,3 +331,10 @@ public function repeat() {
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。
## 使用机器人 API 和事件
如果你想不只是回复消息还要做其他复杂的动作Action使用 OneBot Action又名 OneBot API进行发送即可见 [机器人 API](/component/bot/robot-api)。
如果想处理其他类型的事件,比如 QQ 群通知事件等,见 [机器人注解事件](/event/robot-annotations/)。

37
docs/guide/upgrade.md Normal file
View File

@@ -0,0 +1,37 @@
# 升级指南
因为框架在随着需求以及 Bug 在不断更新,所以在未来框架会发布新版本。为了方便从旧版本安装并使用框架的开发者无损更新到新版本,这里提供了升级版本上需要注意的事项。
## 版本约定
炸毛框架的版本号一般情况均按照 [Semantic Versioning 2.0.0](https://semver.org/) 标准进行滚动发行,规则简述如下:
假设版本号为 x.y.z
- `x` 为大版本,一般只有在发生完全无法兼容的更新时增加,需要开发者最重视。
- `y` 为小发行版本,默认情况下会新增组件功能,但会尽可能兼容旧版本,存在不兼容情况极少。
- `z` 为补丁版本,在不进行任何大功能更新情况下提供 Bug 的修复,完全兼容前版本。
例如,炸毛框架的 `2.4.2` 版本,在 `2.5.0` 发行后,框架提供了大量新组件,但是对旧版本的配置和组件完全兼容,无任何额外的说明,则可以直接升级。
## 升级方法
根据安装方法不同,升级的方法也不同。
框架安装方式有多种,但主要分为三类:
- Composer 加载库的方式
- 框架源码模式
- Phar 打包模式
在 Composer 加载库的方式下,一般是指使用命令 `composer require zhamao/framework``composer create-project zhamao/framework-starter` 的方式安装框架,框架的核心文件都在 `vendor` 目录下。
此方式安装的框架升级最方便,直接执行命令 `composer update` 即可。
框架源码模式安装一般为直接使用 `git clone` 框架本体的 GitHub 仓库或下载 master 分支安装,这种情况不可升级版本(或使用 `git pull` 拉取)。
Phar 打包模式更新则必须重新自行打包新版本,例如从 Composer 加载库方式打包的框架,则需在原目录使用 `composer update` 后再次打包一个新版本。
## 升级提示
如果在升级过程中遇到了提示,则可能是需要升级某些配置文件需要手动进行合并更新。如果提示了更新,建议到 `vendor/zhamao/framework/config/global.php` 框架的最新库内配置文件与 `config/global.php` 文件进行对比。

View File

@@ -1,18 +1,12 @@
# 介绍
> 本文档为炸毛框架 v2 版本,如需查看 v1 版本,[点我](https://docs-v1.zhamao.xin/)。
> 如果是从 v1.x 版本升级到 v2.x[点我看升级指南](/advanced/to-v2/)。
!!! tip "提示"
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP/WS 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
在 HTTP 和 WebSocket 服务器上PHP 的扩展 Swoole 提供了高性能的支持,使其效率可媲美 nginx 静态网页处理的效率。
框架主要用途为 HTTP/WebSocket 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
@@ -38,18 +32,18 @@ public function index() {
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行(会跑 Linux 程序)
2. php 7.2+ 开发环境(项目会持续支持最新的 PHP 版本)
3. HTTP/WebSocket 协议
2. php >=7.2 开发环境(项目会持续支持最新的 PHP 版本)
4. OneBot 机器人聊天接口标准
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。
!!! bug "文档提示"
此文档采用 MkDocs 驱动,但因为本文档的搜索组件原生不支持中文搜索,所以搜索体验会大打折扣,敬请谅解!搜不到不是没这个东西
此文档采用 MkDocs 驱动,文档的搜索组件原生不支持中文搜索,且分词很难控制,所以搜索体验会大打折扣,敬请谅解!搜不到不是没这个东西,建议这种情况可以自行翻阅目录查看
## 框架特色
- 支持MySQL数据库连接池自带查询缓存提高多查询时的效率
- Websocket 服务器、HTTP 服务器兼容运行,一个框架多个用处
- 支持命令、自然语言处理等多种插件形式
@@ -61,6 +55,7 @@ public function index() {
## 文档主题
### 主题
<div class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
@@ -80,6 +75,7 @@ public function index() {
</script>
### 主色调
<div class="tx-switch">
<button data-md-color-primary="red"><code>red</code></button>
<button data-md-color-primary="pink"><code>pink</code></button>
@@ -105,6 +101,7 @@ public function index() {
</div>
### 辅色调
<div class="tx-switch"> <button data-md-color-accent="red"><code>red</code></button> <button data-md-color-accent="pink"><code>pink</code></button> <button data-md-color-accent="purple"><code>purple</code></button> <button data-md-color-accent="deep-purple"><code>deep purple</code></button> <button data-md-color-accent="indigo"><code>indigo</code></button> <button data-md-color-accent="blue"><code>blue</code></button> <button data-md-color-accent="light-blue"><code>light blue</code></button> <button data-md-color-accent="cyan"><code>cyan</code></button> <button data-md-color-accent="teal"><code>teal</code></button> <button data-md-color-accent="green"><code>green</code></button> <button data-md-color-accent="light-green"><code>light green</code></button> <button data-md-color-accent="lime"><code>lime</code></button> <button data-md-color-accent="yellow"><code>yellow</code></button> <button data-md-color-accent="amber"><code>amber</code></button> <button data-md-color-accent="orange"><code>orange</code></button> <button data-md-color-accent="deep-orange"><code>deep orange</code></button> </div>
<script>

File diff suppressed because one or more lines are too long

190
docs/update/build-update.md Normal file
View File

@@ -0,0 +1,190 @@
# 更新日志master 分支 commit
此文档将显示非发布版的提交版本相关更新文档,可能与发布版的更新日志有重合,在此仅作更新记录。
同时此处将只使用 build 版本号进行区分。
## build 447 (2022-3-20)
- 发布 2.7.0 正式版
## build 446 (2022-3-20)
- 新增 `./zhamao server` 下的 `--no-state-check` 参数,关闭“启动框架前的运行状态检查”功能
## build 445 (2022-3-20)
- 新增配置项 `runtime`.`annotation_reader_ignore`:支持注解解析器忽略注解的自定义
## build 444 (2022-3-20)
- 更改 `extra`.`exclude_annotate``zm`.`exclude-annotation-path`
## build 443 (2022-3-20)
- 修复注释空格的样式
## build 442 (2022-3-20)
- 修复打包模块后 `files` 的 autoload 项不能被解压和引入的 Bug
## build 441 (2022-3-20)
- 修复打包模块时命名空间与实际不一致的 Bug
## build 440 (2022-3-20)
- 新增方法宏Macroable
## build 439 (2022-3-19)
- 新增 PHP 8 Attribute 与注解同时支持的特性
## build 438 (2022-3-18)
- 修复 Response 类在 PHP 8.1 环境下的报错
## build 437 (2022-3-17)
- 修复 `ctx()` 可能会返回 null 的 Bug
## build 436 (2022-3-15)
- 新增 PHPStan 和 PHP CS Fixer 并优化全局代码
## build 435 (2022-3-13)
- 优化分离 WorkerManager 与 ProcessManager 的职责
- 新增 Ctrl+C 一次无法停止框架时多次 Ctrl+C 后可强行杀掉所有进程的功能
- `./zhamao server:stop` 新增参数 `--force`,使用 `SIGKILL` 强行杀掉所有进程
- 新增 AnnotationParser 对 `autoload-dev` 项中的 `psr-4` 默认检索条件
- 新增框架启动状态检测功能,如果已经启动了同样目录的框架,则会报错
- 新增“强制启用轮询模式启动热更新”功能(参数 `--polling-watch`
- 修复与 PHP 8.1 的兼容性
- 对 DaemonCommand 进行优化,与 ServerCommand 效果相同
- 修复 `autoload`.`psr-4` 不存在时报错的 Bug
- 新增框架停止时 Worker 退出回显状态码
- 新增 inotify 判断模式,如果使用 `--watch` 检测到没有安装 inotify则自动使用轮询模式
## build 434 (2022-1-8)
- 修复框架在 PHP 8.1 下运行时的一些问题
- 新增 Console 日志输出到文件的功能
## build 433 (2021-12-28)
- 修复 OneBotV11 因 IDE 自动优化导致 API 接口发生变化的问题
## build 432 (2021-12-25)
- 新增 GoCqhttpAPI 包,用于支持额外的 OneBot ActionAPI
- 修复 MySQL 查询器中 `fetchOne()` 方法无法正确返回值的 Bug
- 修复 Swoole Hook 因配置不当无法正确使用的 Bug
## build 431 (2021-12-22)
- 修复 Issue #50
- 新增 PhpStorm IDE 直接运行框架的脚本
## build 430 (2021-12-8)
- 删除调试信息
- 修复 #56 中关于数据库组件的 Bug
## build 429 (2021-12-7)
- 新增配置项 `onebot`.`message_command_policy`
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
## build 428 (2021-11-16)
- 修复 `ctx()->waitMessage()` 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 `ctx()->getArrayMessage()``ctx()->getStringMessage()` 两个方法
- 修复注解事件 `CQCommand``CQMessage` 在 array 消息模式下无法正确解析的 Bug
## build 427 (2021-11-16)
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## build 426 (2021-11-10)
- 修复 CQ 码的解析函数 Bug#52
## build 425 (2021-11-3)
- 删除未实际应用功能的配置参数
- 修复 reload 时会断开 WebSocket 连接且导致进程崩溃的 Bug
## build 424 (2021-11-2)
- 新增 InstantModule 类、ZMServer 类、ModuleBase 类
- 配置文件新增 `runtime.reload_kill_connect``runtime.global_middleware_binding` 选项
- 修复部分情况下闭包事件分发时崩溃的 bug
- 新增内部方法 `_zm_env_check`
- 调整默认的 OneBot 模块对应的等级从 99999 调整为 99
- 新增导出框架运行参数的列表功能
## build 423 (2021-10-17)
- 修复 PHP 7.2 ~ 7.3 下无法使用新版 MySQL 组件的 bug
## build 422 (2021-10-6)
- 修复 `script_` 前缀无法被排除加载模块的 bug
- 修复 MySQL 组件的依赖问题
## build 421 (2021-9-11)
- 删除多余的调试信息
## build 420 (2021-9-11)
- 修复 OneBot 事件无法响应的 bug
- 新增部分 EventDispatcher 触发的事件 debug 日志
## build 419 (2021-9-11)
- 修复 DB 模块在未连接数据库的时候抛出未知异常
- 修复部分情况下打包模块出现的错误
## build 418 (2021-9-10)
- 修复 ZMAtomic 在 test 环境下的 bug
- 修复 MessageUtil 的报错
## build 417 (2021-8-29)
- 新增 AnnotationException统一框架内部的抛出异常的类型
- 新增 AnnotationParser 下的 `verifyMiddlewares()` 方法
- 私有化 CQAPI 类下的内部方法
- 将 WebSocket API 响应超时时间从 60 秒缩短为 30 秒
- 修复 DB 类不能使用旧查询器的 bug
- 统一 DB 类下抛出 Exception 的类型为 ZMException 的子类
- EventDispatcher 新增对 `middleware_error_policy` 的处理段
- 配置文件下 `runtime` 新增 `middleware_error_policy` 字段
- 将 LightCache 组件抛出的异常改为 LightCacheException
- ModuleManager 修复改配置的 `load_path` 不生效的 bug
- 修复打包时生成的 Phar Autoload 列表出错的 bug
- 将配置的 override 改为 overwrite
- 新增解包时忽略依赖的选项(`--ignore-depends`
- 删除众多调试日志,修改部分调试日志为 debug 级别的输出
- 修改 `ZM\MySQL\MySQLManager` 下的 `getConnection()``getWrapper()`
- MySQLPool 对象新增 `getCount()` 方法
- 新增 MySQLQueryBuilder 类(`doctrine/dbal` 的 wrapper 类)
- 修复 MySQLStatement 封装原 dbal 组件时与连接池不兼容的 bug
- 新增 MySQLStatementWrapper 类
- 完善 MySQLWrapper 类,用作主要的查询对象控制类
- 编写外部插件加载方式Phar 热加载功能)
- 修复 `ZMUtil::getClassesPsr4()` 方法在遇到空扩展名文件时的报错

View File

@@ -2,6 +2,59 @@
这里将会记录各个主版本的框架升级后,涉及 `global.php` 的更新日志,你可以根据这里描述的内容与你的旧配置文件进行合并。
## v2.7.0 (build 447)
- 新增 `$config['runtime']` 下的 `annotation_reader_ignore` 项。
## v2.6.6 (build 434)
- 新增 `$config['runtime']` 下的 `save_console_log_file` 项。
## v2.6.0 (build 427)
- 新增 `$config['runtime']` 下的 `reload_delay_time``global_middleware_binding` 项。
- 新增 `$config['onebot']` 下的 `message_convert_string` 项。
## v2.5.1 (build 417)
- 新增 `$config['runtime']` 下的 `middleware_error_policy` 选项。
## v2.5.0 (build 413)
- 新增 `$config['runtime']` 运行时设置。
- 删除 `$config['server_event_handler_class']`,默认在启动时全局扫描。
- 新增 `$config['module_loader']` 模块/插件 打包配置选项。
- 新增 `$config['mysql_config']`,取代原先的 `$config['sql_config']`此外废弃原先的MySQL 查询器 `\ZM\DB\DB` 类。
更新部分:
```php
/** 一些框架与Swoole运行时设置的调整 */
$config['runtime'] = [
'swoole_coroutine_hook_flags' => SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL),
'swoole_server_mode' => SWOOLE_PROCESS
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['mysql_config'] = [
'host' => '',
'port' => 3306,
'unix_socket' => null,
'username' => 'root',
'password' => '123456',
'dbname' => 'adb',
'charset' => 'utf8mb4',
'pool_size' => 64,
'options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
];
/** 注册 Swoole Server 事件注解的类列表(deleted) */
// 删除
```
## v2.4.0 (build 400)
- 调整 `$config['modules']['onebot']` 配置项到 `$config['onebot']`,旧版本的此段会向下兼容,建议更新,
- 新增 `$config['remote_terminal']` 远程终端的配置项,新增此段即可。

View File

@@ -1,5 +1,268 @@
# 更新日志v2 版本)
## v2.7.0build 447
> 更新时间2022.3.20
- 优化分离 WorkerManager 与 ProcessManager 的职责
- 新增 Ctrl+C 一次无法停止框架时多次 Ctrl+C 后可强行杀掉所有进程的功能
- `./zhamao server:stop` 新增参数 `--force`,使用 `SIGKILL` 强行杀掉所有进程
- 新增 AnnotationParser 对 `autoload-dev` 项中的 `psr-4` 默认检索条件
- 新增框架启动状态检测功能,如果已经启动了同样目录的框架,则会报错
- 新增“强制启用轮询模式启动热更新”功能(参数 `--polling-watch`
- 修复与 PHP 8.1 的兼容性
- 对 DaemonCommand 进行优化,与 ServerCommand 效果相同
- 修复 `autoload`.`psr-4` 不存在时报错的 Bug
- 新增框架停止时 Worker 退出回显状态码
- 新增 inotify 判断模式,如果使用 `--watch` 检测到没有安装 inotify则自动使用轮询模式
- 新增 PHPStan 和 PHP CS Fixer 并优化全局代码
- 修复 `ctx()` 可能会返回 null 的 Bug
- 修复 Response 类在 PHP 8.1 环境下的报错
- 新增 PHP 8 Attribute 与注解同时支持的特性
- 新增方法宏Macroable
- 修复打包模块时命名空间与实际不一致的 Bug
- 修复打包模块后 `files` 的 autoload 项不能被解压和引入的 Bug
- 修复注释空格的样式
- 更改 `extra`.`exclude_annotate``zm`.`exclude-annotation-path`
- 新增配置项 `runtime`.`annotation_reader_ignore`:支持注解解析器忽略注解的自定义
- 新增 `./zhamao server` 下的 `--no-state-check` 参数,关闭“启动框架前的运行状态检查”功能
## v2.6.6build 434
> 更新时间2022.1.8
- 修复框架在 PHP 8.1 下运行时的一些问题
- 新增 Console 日志输出到文件的功能
## v2.6.5build 433
> 更新时间2021.12.28
- 修复 OneBotV11 因 IDE 自动优化导致 API 接口发生变化的问题
## v2.6.4build 432
> 更新时间2021.12.25
- 新增 GoCqhttpAPI 包,用于支持额外的 OneBot ActionAPI
- 修复 MySQL 查询器中 `fetchOne()` 方法无法正确返回值的 Bug
- 修复 Swoole Hook 因配置不当无法正确使用的 Bug
- 修复 Issue #50
- 新增 PhpStorm IDE 直接运行框架的脚本
## v2.6.3 (build 430)
> 更新时间2021.12.8
- 删除调试信息
- 修复 #56 中关于数据库组件的 Bug
## v2.6.2 (build 429)
> 更新时间2021.12.7
- 新增配置项 `onebot`.`message_command_policy`
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
## v2.6.1 (build 428)
> 更新时间2021.11.16
- 修复 ctx()->waitMessage() 在 array 消息模式下无法正确返回消息字符串的问题
- 新增 ctx()->getArrayMessage() 和 ctx()->getStringMessage() 两个方法
- 修复注解事件 CQCommand 和 CQMessage 在 array 消息模式下无法正确解析的 Bug
## v2.6.0 (build 427)
> 更新时间2021.11.16
- 新增全局中间件,可在全局配置文件中设置
- 修复部分 Typo
- 新增指令 `server:status``server:reload``server:stop` 可用在新开终端中查看框架运行状态、重启和退出
- 新增支持 `array` 格式的消息
- 上下文 Context 对象新增 `getOriginMessage()` 用于获取原消息,`getMessage()` 如果在设置了转换后,将默认转换消息为字符串格式保持与旧模块兼容
- OneBot API 新增全局过滤器,可用作 Action 过滤重写等操作
- 配置文件新增 `runtime.reload_delay_time`,用于可配置重载 Worker 等待的时间(毫秒)
- 配置文件新增 `runtime.global_middleware_binding`,用于配置全局中间件
- 配置文件新增 `onebot.message_convert_string`,用于配置是否转换数组格式为字符串,保证与前版本的兼容性(默认为 true
- MessageUtil 消息工具类新增方法:`strToArray($msg, bool $ignore_space = true, bool $trim_text = false)`
- MessageUtil 消息工具类新增方法:`arrayToStr(array $array)`
- 新增框架启动多次监测功能,无法使用同一个框架项目同时启动两个框架
## v2.5.8 (build 426)
> 更新时间2021.11.10
- 修复 CQ 码的解析函数 Bug#52
## v2.5.7 (build 425)
> 更新时间2021.11.3
- 调低 OneBot 相关事件在 Swoole 的优先级
- 修复部分情况下闭包事件函数分发时引发的崩溃 bug
- 修复 reload 时会断开 WebSocket 连接且导致进程崩溃的 bug
## v2.5.6 (build 423)
> 更新时间2021.10.17
- 修复 PHP 7.2 ~ 7.3 下无法使用新版 MySQL 组件的 bug
## v2.5.5 (build 422)
> 更新时间2021.10.6
- 修复 `script_` 前缀无法被排除加载模块的 bug
- 修复 MySQL 组件的依赖问题
## v2.5.4 (buidl 421)
> 更新时间2021.9.11
- 删除多余的调试信息
## v2.5.3 (build 420)
> 更新时间2021.9.11
- 修复 DB 模块在未连接数据库的时候抛出未知异常
- 修复部分情况下打包模块出现的错误
- 修复 OneBot 事件无法响应的 bug
- 新增部分 EventDispatcher 触发的事件 debug 日志
## v2.5.2 (build 418)
> 更新时间2021.9.10
- 新增 AnnotationException统一框架内部的抛出异常的类型
- 新增 AnnotationParser 下的 `verifyMiddlewares()` 方法
- 私有化 CQAPI 类下的内部方法
- 将 WebSocket API 响应超时时间从 60 秒缩短为 30 秒
- 修复 DB 类不能使用旧查询器的 bug
- 统一 DB 类下抛出 Exception 的类型为 ZMException 的子类
- EventDispatcher 新增对 `middleware_error_policy` 的处理段
- 配置文件下 `runtime` 新增 `middleware_error_policy` 字段
- 将 LightCache 组件抛出的异常改为 LightCacheException
- ModuleManager 修复改配置的 `load_path` 不生效的 bug
- 修复打包时生成的 Phar Autoload 列表出错的 bug
- 将配置的 override 改为 overwrite
- 新增解包时忽略依赖的选项(`--ignore-depends`
- 删除众多调试日志,修改部分调试日志为 debug 级别的输出
- 修改 `ZM\MySQL\MySQLManager` 下的 `getConnection()``getWrapper()`
- MySQLPool 对象新增 `getCount()` 方法
- 新增 MySQLQueryBuilder 类(`doctrine/dbal` 的 wrapper 类)
- 修复 MySQLStatement 封装原 dbal 组件时与连接池不兼容的 bug
- 新增 MySQLStatementWrapper 类
- 完善 MySQLWrapper 类,用作主要的查询对象控制类
- 编写外部插件加载方式Phar 热加载功能)
- 修复 `ZMUtil::getClassesPsr4()` 方法在遇到空扩展名文件时的报错
## v2.5.1 (build 416)
> 更新时间2021.7.9
- 修复:脚手架无法正常使用 `init` 命令的 bug。
## v2.5.0build 415
> 更新时间2021.7.9
以下是版本**新增内容**
- 新增全新的模块系统可打包模块src 目录下的子目录用户逻辑代码)为 phar 格式进行分发和版本备份。
- 全局配置文件新增 `module_loader` 项,用于配置外部模块加载的一些设置。
- 全局配置文件新增 `runtime` 配置项,可自定义配置 Swoole 的一些运行时参数,目前可配置一键协程化的 Hook 参数和 Swoole Server 的启动模式。
- 新增 `module:list` 命令,用于查看未打包和已打包的模块列表。
- 新增 `module:pack` 命令,用于打包现有 src 目录下的模块。
- 新增 `module:unpack` 命令,用于解包现有的 phar 模块包。
- 新增打包框架功能,支持将用户的整个项目连同炸毛框架打包为一个 phar 便携运行,使用命令 `build`
- 新增快捷脚本 `./zhamao`,效果同 `vendor/bin/start``bin/start`
- 新增启动参数 `--interact`:又重新支持交互终端了,但还是有点问题,不推荐使用。
- 新增启动参数 `--disable-safe-exit`:如果你的项目在 Ctrl+C 时总是卡住且项目内没有什么使用 LightCache 等缓存在内存的数据可开启防止关不掉框架。
- 新增启动参数 `--preview`:只显示参数,不启动炸毛框架的服务器。
- 新增启动参数 `--force-load-module`:强制打包状态下加载的模块(使用英文逗号分隔多个模块名称)。
- `CoroutinePool` 协程池新增 `getRunningCoroutineCount` 方法,用于查看协程池中的协程数量。
- `DataProvider` 新增 `getFrameworkRootDir()``getSourceRootDir()`,分别代表获取框架的根目录和用户源码根目录。(详见下方对目录的定义解释)
- `DataProvider``getDataFolder` 新增参数 `$second = ''`,如果给定,则自动创建子目录 `$second` 并返回。
- `DataProvider` 新增 `scanDirFiles()` 方法,用于扫描目录,可选择是否递归、是否返回相对路径,也支持扫描 Phar 文件内的路径,非常好用。
- `DataProvider` 新增 `isRelativePath()` 方法,检查路径是否为相对路径(根据第一个字符是否是 '/' 来判断)。
- `ZMUtil` 新增 `getClassesPsr4()` 方法,用于根据 Psr-4 标准来获取目录下的所有类文件。
- 新增全局错误码,可以根据错误码在文档内快速定位和解决问题。
- 中间件和注解事件支持回溯,可以快速查看调用栈(比如中间件可以知道自己是在哪个注解事件中被调用)。
- 使用 `./zhamao build` 来构建框架的 phar 包时增加显示进度条。
- EventDispatcher 新增方法 `getEid()``getClass()`,分别用于获取事件分发 ID 和注解事件的注解类名称。
- 新增 EventTracer用于追踪事件的调用栈。
- 中间件支持传参。
- MySQL 数据库查询器改为使用 `doctrine/dbal` 组件,更灵活和稳定。
- 新增对 `SWOOLE_BASE` 模式的支持(支持只启动一个进程的 Server
以下是版本**修改内容**
- 启动文件 `vendor/bin/start` 修改为 shell 脚本,可自动寻找 PHP 环境。
- 全局强制依赖 `league/climate` 组件。
- 修复框架启动时的信息显示换行问题。
- 修复框架使用 Phar 方式启动时导致的报错。
- 修复使用 Ctrl+C 结束时一部分用户卡住的 bug。
- 远程和本地终端去掉 stop 命令,建议直接使用发 SIGTERM 方式结束框架。
- 全局配置文件的 `zm_data` 根目录默认修改为 `WORKING_DIR`
- 命令 `systemd:generate` 修改为 `generate:systemd`
- 全局配置文件删除 `server_event_handler_class` 项,此项废弃。
- 修复部分 CQ 码解析过程中没有转义的问题。
-`ZMRobot` 类转移为 `OneBotV11` 类,但提供兼容。
- 修复在守护进程模式下使用 `daemon:reload``daemon:stop` 命令可能失效的问题。
- 修复 systemd 生成时脚本目录错误的 bug。
- 修复 PipeMessage 等事件未捕获错误导致崩溃的问题。
- `ZM\Http\RouteManager` 移动到 `ZM\Utils\Manager\RouteManager`,但原地址兼容。
- 修复 `Terminal` 类使用的一些问题。
-`pcntl` 扩展改为可选依赖,当 Swoole 版本大于等于 4.6.7 时不需要安装 `pcntl` 扩展。
- 修正启动时框架对缺省配置项的一些默认参数。
- 注解 `@OnSetup``@SwooleHandler` 可直接使用,无需设置 `server_event_handler_class` 即可。
- 修复框架在一些非正常终端中运行时导致错误的问题。
- 使用 `--debug-mode` 参数时,自动开启热更新。
- 修复脚手架在使用 composer 更新后检查全局配置功能的 bug。
- 修复重启和关闭框架时造成的非正常连接断开。
- 改用独立进程监听文件变化和终端输入。
- 修复有协程中断的任务时停止服务器会报 Swoole 警告的 bug。
- 修复连接被反复断开的问题。
**对目录的定义解释**
在 2.4.4 版本之前,使用炸毛框架中,只含有两种目录,`getWorkingDir``getDataFolder`,分别代表获取工作目录和数据目录。在 2.5 版本中,又新增了 `getFrameworkRootDir` 代表获取框架的根目录,`getSourceRootDir` 代表获取源码的根目录。
以 Composer 运行模式举例,如果你使用 `composer create-project zhamao/framework-starter` 命令新建的框架,那么假设我们从 `/app` 目录下运行此命令,然后使用 `cd framework-starter/` 进入项目目录,此时我们使用 `vendor/bin/start server` 命令运行服务器,对应的目录为:
- `WorkingDir``/app/framework-starter/`
- `SourceRootDir``/app/framework-starter/`
- `FrameworkRootDir``/app/framework-starter/vendor/zhamao/framework/`
如果以源码模式(直接克隆 `zhamao-framework.git` 仓库),启动框架,那么使用命令 `bin/start server` 启动框架后,以上三个返回的目录则完全相同。
如果以 2.5 版本新的项目归档模式build启动框架假设我们的项目代码打包为 `server.phar`,在 `/app/` 目录,我们使用命令 `php server.phar server` 启动炸毛框架,那么它对应的目录为:
- `WorkingDir``/app/`
- `SourceRootDir``phar:///app/server.phar/`
- `FrameworkRootDir``phar:///app/server.phar/vendor/zhamao/framework/`
如果最后一种归档方式启动的框架是从源码模式打包而来,那么 `FrameworkRootDir` 就与 `SourceRootDir` 相同。
**版本部分兼容问题变化**
理论上如果不使用框架内部未开放的接口方法的话,从 2.4 升级到 2.5 是非常自然的,但是也有一部分可能会造成不兼容的问题。
- 生成 systemd 配置文件的命令 `systemd:generate` 变成 `generate:systemd`
- 全局配置文件中的 `zm_data` 的父目录由 `__DIR__ . "/../"` 改为 `WORKING_DIR`
- 2.5 版本将 ZMRobot 类中的所有函数方法都移动到了 `OneBotV11` 类中,但原先的 ZMRobot 还可以使用。
## v2.4.4 (build 405)
> 更新时间2021.3.29
以下是可能不兼容的变更:
- 新增依赖:框架需要 PHP 安装 pcntl 扩展以及开启 `pcntl_signal` 函数(一般情况下编译安装的都会有,宝塔面板请手动解除函数禁用)
## v2.4.3 (build 403)
> 更新时间2021.3.29
@@ -275,4 +538,4 @@
> 更新时间2020.12.23
已发布正式版。
已发布正式版。

View File

@@ -0,0 +1,34 @@
#!/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

58
install-runtime.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/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
}
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=7.4&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"

26
instant-demo.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once 'vendor/autoload.php';
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Module\InstantModule;
use ZM\ZMServer;
$weather = new InstantModule('weather');
$weather->onEvent(OnOpenEvent::class, ['connect_type' => 'qq'], function (ConnectionObject $conn) {
Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!');
});
$weather->onEvent(CQCommand::class, ['match' => '你好'], function () {
ctx()->reply('hello呀');
});
$app = new ZMServer('app-name');
$app->addModule($weather);
$app->run();

View File

@@ -1,4 +1,4 @@
site_name: 炸毛框架 v2
site_name: 炸毛框架文档
repo_name: '炸毛框架'
repo_url: 'https://github.com/zhamao-robot/zhamao-framework'
@@ -14,27 +14,32 @@ theme:
accent: indigo
features:
- navigation.tabs
- navigation.sections
extra_javascript:
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/highlight.min.js
- javascripts/library/highlight.min.js
- javascripts/config.js
extra_css:
- assets/css/extra.css
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.1.1/styles/default.min.css
- assets/css/library/default.min.css
plugins:
- search:
lang: ja
markdown_extensions:
- admonition
- pymdownx.tabbed
- pymdownx.superfences
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.details
- abbr
- pymdownx.highlight:
linenums: true
linenums_style: pymdownx.inline
extra:
version:
method: mike
provider: mike
copyright: 'Copyright &copy; 2019 - 2021 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
copyright: 'Copyright &copy; 2019 - 2022 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="tx-switch">
<button data-md-color-scheme="default"><code>默认模式</code></button>
<button data-md-color-scheme="slate"><code>暗黑模式</code></button>
</span>
@@ -50,7 +55,7 @@ copyright: 'Copyright &copy; 2019 - 2021 CrazyBot Team&nbsp;&nbsp;&nbsp;&nbsp;&n
name.textContent = attr;
})
})
</script><br><a href="http://beian.miit.gov.cn">ICP备18000198号-1</a>'
</script><br><a href="http://beian.miit.gov.cn">ICP备2021010446号-1</a>'
nav:
- 指南:
@@ -62,6 +67,8 @@ nav:
- 基本配置: guide/basic-config.md
- 编写模块: guide/write-module.md
- 注册事件响应: guide/register-event.md
- 升级指南: guide/upgrade.md
- 错误码对照表: guide/errcode.md
- 事件和注解:
- 事件和注解: event/index.md
- 机器人注解事件: event/robot-annotations.md
@@ -72,43 +79,65 @@ nav:
- 事件分发器: event/event-dispatcher.md
- 框架组件:
- 框架组件: component/index.md
- 上下文: component/context.md
- 聊天机器人组件:
- 机器人 API: component/robot-api.md
- CQ 码(多媒体消息: component/cqcode.md
- 机器人消息处理: component/message-util.md
- Token 验证: component/access-token.md
- 机器人动作V12: component/bot/robot-api-12.md
- 机器人动作V11: component/bot/robot-api.md
- CQ 码(多媒体消息): component/bot/cqcode.md
- 机器人消息处理: component/bot/message-util.md
- Token 验证: component/bot/access-token.md
- 图灵机器人 API: component/bot/turing-api.md
- 存储:
- LightCache 轻量缓存: component/light-cache.md
- MySQL 数据库: component/mysql.md
- Redis 数据库: component/redis.md
- ZMAtomic 原子计数器: component/atomics.md
- SpinLock 自旋锁: component/spin-lock.md
- 文件管理: component/data-provider.md
- LightCache 轻量缓存: component/store/light-cache.md
- MySQL 查询器:
- 简介: component/store/mysql/mysql.md
- 配置: component/store/mysql/config.md
- 执行 SQL 语句: component/store/mysql/common-query.md
- MySQL 查询器(废弃): component/store/mysql-db.md
- Redis 数据库: component/store/redis.md
- ZMAtomic 原子计数器: component/store/atomics.md
- SpinLock 自旋锁: component/store/spin-lock.md
- 文件管理: component/store/data-provider.md
- 通用组件:
- 上下文: component/common/context.md
- 协程池: component/common/coroutine-pool.md
- 单例类: component/common/singleton-trait.md
- ZMUtil 杂项: component/common/zmutil.md
- 全局方法: component/common/global-functions.md
- Console 终端: component/common/console.md
- TaskWorker 管理: component/common/task-worker.md
- Terminal 终端: component/common/remote-terminal.md
- EventTracer 事件追踪器: component/common/event-tracer.md
- HTTP 服务器工具类:
- HTTP 和 WebSocket 客户端: component/zmrequest.md
- HTTP 路由管理: component/route-manager.md
- 协程池: component/coroutine-pool.md
- 单例类: component/singleton-trait.md
- ZMUtil 杂项: component/zmutil.md
- 全局方法: component/global-functions.md
- Console 终端: component/console.md
- TaskWorker 管理: component/task-worker.md
- 远程终端: component/remote-terminal.md
- HTTP 和 WebSocket 客户端: component/http/zmrequest.md
- HTTP 路由管理: component/http/route-manager.md
- 模块/插件管理:
- 模块打包: component/module/module-pack.md
- 模块解包: component/module/module-unpack.md
- 进阶开发:
- 进阶开发: advanced/index.md
- 框架剖析: advanced/framework-structure.md
- 框架启动模式: advanced/custom-start.md
- 从 v1 升级: advanced/to-v2.md
- 内部类文件手册: advanced/inside-class.md
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 框架多进程: advanced/multi-process.md
- TaskWorker 提高并发: advanced/task-worker.md
- 框架高级开发:
- 框架剖析: advanced/framework-structure.md
- 框架启动模式: advanced/custom-start.md
- 手动安装环境: advanced/manually-install.md
- 内部类文件手册: advanced/inside-class.md
- 框架多进程: advanced/multi-process.md
- TaskWorker 提高并发: advanced/task-worker.md
- 开发实战教程:
- 接入 WebSocket 客户端: advanced/connect-ws-client.md
- 编写管理员才能触发的功能: advanced/example/admin.md
- FAQ: FAQ.md
- 接入青云客聊天API: advanced/example/integrate-qingyunke-chatbot.md
- 开发天气机器人: advanced/example/weather-bot.md
- FAQ:
- FAQ: faq/FAQ.md
- 从 v1 升级: faq/to-v2.md
- 框架常见问题(持续更新): faq/usual-question.md
- 启动时报错 Address already in use: faq/address-already-in-use.md
- 出现 deadlock 字样: faq/display-deadlock.md
- 使用 LightCache 关闭时无法正常保存持久化: faq/light-cache-wrong.md
- CQBefore 过滤不了 waitMessage: faq/wait-message-cqbefore.md
- 更新日志:
- 更新日志v2: update/v2.md
- 更新日志v1: update/v1.md
- 更新日志build update: update/build-update.md
- 配置文件更新日志: update/config.md
- <u>炸毛框架 v1</u>: https://docs-v1.zhamao.xin/

7
phpstan.neon Normal file
View File

@@ -0,0 +1,7 @@
parameters:
reportUnmatchedIgnoredErrors: false
level: 0
paths:
- ./src/
ignoreErrors:
- '#Used constant OS_TYPE_(LINUX|WINDOWS) not found#'

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace Custom\Annotation;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\CustomAnnotation;
@@ -10,11 +13,17 @@ use ZM\Annotation\Interfaces\CustomAnnotation;
/**
* Class CustomAnnotation
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
* @package Custom\Annotation
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class Example extends AnnotationBase implements CustomAnnotation
{
/** @var string */
public $str = '';
public function __construct($str = '')
{
$this->str = $str;
}
}

View File

@@ -1,11 +1,10 @@
<?php /** @noinspection PhpFullyQualifiedNameUsageInspection */ #plain
<?php
//这里写你的全局函数
/**
* @param callable $func
* @param string $name
* @noinspection PhpUnused
*/
function pgo(callable $func, $name = "default") {
\ZM\Utils\CoroutinePool::go($func, $name);
declare(strict_types=1);
use ZM\Utils\CoroutinePool;
function pgo(callable $func, string $name = 'default')
{
CoroutinePool::go($func, $name);
}

View File

@@ -1,19 +1,22 @@
<?php /** @noinspection PhpMissingReturnTypeInspection */
<?php
declare(strict_types=1);
namespace Module\Example;
use ZM\Annotation\CQ\CQBefore;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\CQ\CQMessage;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnCloseEvent;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\API\CQ;
use ZM\API\OneBotV11;
use ZM\API\TuringAPI;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\RequestMapping;
use ZM\Event\EventDispatcher;
use ZM\Exception\InterruptException;
use ZM\Module\QQBot;
@@ -23,7 +26,6 @@ use ZM\Utils\ZMUtil;
/**
* Class Hello
* @package Module\Example
* @since 2.0
*/
class Hello
@@ -32,45 +34,57 @@ class Hello
* 默认的图片监听路由对应目录,如需要使用可取消下面的注释,把上面的 /* 换成 /**
* @OnStart(-1)
*/
//public function onStart() {
// public function onStart() {
// \ZM\Http\RouteManager::addStaticFileRoute("/images/", \ZM\Utils\DataProvider::getWorkingDir()."/zm_data/images/");
//}
// }
/**
* 使用命令 .reload 发给机器人远程重载,注意将 user_id 换成你自己的 QQ
* @CQCommand(".reload",user_id=627577391)
*/
public function reload() {
ctx()->reply("重启中...");
public function reload()
{
ctx()->reply('重启中...');
ZMUtil::reload();
}
/**
* @CQCommand("我是谁")
*/
public function whoami() {
$user = ctx()->getRobot()->getLoginInfo();
return "你是" . $user["data"]["nickname"] . "QQ号是" . $user["data"]["user_id"];
public function whoami()
{
$bot = ctx()->getRobot()->getLoginInfo();
$bot_id = $bot['data']['user_id'];
$r = OneBotV11::get($bot_id);
$QQid = ctx()->getUserId();
$nick = $r->getStrangerInfo($QQid)['data']['nickname'];
return '你是' . $nick . 'QQ号是' . $QQid;
}
/**
* 向机器人发送"你好啊",也可回复这句话
* @CQCommand(match="你好",alias={"你好啊","你是谁"})
*/
public function hello() {
return "你好啊,我是由炸毛框架构建的机器人!";
public function hello()
{
return '你好啊,我是由炸毛框架构建的机器人!';
}
/**
* 一个最基本的第三方 API 接口使用示例
* @CQCommand("一言")
*/
public function hitokoto() {
$api_result = ZMRequest::get("https://v1.hitokoto.cn/");
if ($api_result === false) return "接口请求出错,请稍后再试!";
public function hitokoto()
{
$api_result = ZMRequest::get('https://v1.hitokoto.cn/');
if ($api_result === false) {
return '接口请求出错,请稍后再试!';
}
$obj = json_decode($api_result, true);
if ($obj === null) return "接口解析出错!可能返回了非法数据!";
return $obj["hitokoto"] . "\n----「" . $obj["from"] . "";
if ($obj === null) {
return '接口解析出错!可能返回了非法数据!';
}
return $obj['hitokoto'] . "\n----「" . $obj['from'] . '」';
}
/**
@@ -78,32 +92,36 @@ class Hello
* @CQCommand(start_with="机器人",end_with="机器人",message_type="group")
* @CQMessage(message_type="private",level=1)
*/
public function turingAPI() {
public function turingAPI()
{
$user_id = ctx()->getUserId();
$api = ""; // 请在这里填入你的图灵机器人的apikey
if ($api === "") return false; //如果没有填入apikey则此功能关闭
$api = ''; // 请在这里填入你的图灵机器人的apikey
if ($api === '') {
return false;
} // 如果没有填入apikey则此功能关闭
if (($this->_running_annotation ?? null) instanceof CQCommand) {
$msg = ctx()->getFullArg("我在!有什么事吗?");
$msg = ctx()->getFullArg('我在!有什么事吗?');
} else {
$msg = ctx()->getMessage();
}
ctx()->setMessage($msg);
if (MessageUtil::matchCommand($msg, ctx()->getData())->status === false) {
return TuringAPI::getTuringMsg($msg, $user_id, $api);
} else {
QQBot::getInstance()->handle(ctx()->getData(), ctx()->getCache("level") + 1);
return true;
}
QQBot::getInstance()->handle(ctx()->getData(), ctx()->getCache('level') + 1);
// 执行嵌套消息,递归层级+1
return true;
}
/**
* 响应at机器人的消息
* @CQBefore("message")
*/
public function changeAt() {
public function changeAt()
{
if (MessageUtil::isAtMe(ctx()->getMessage(), ctx()->getRobotId())) {
$msg = str_replace(CQ::at(ctx()->getRobotId()), "", ctx()->getMessage());
ctx()->setMessage("机器人" . $msg);
$msg = str_replace(CQ::at(ctx()->getRobotId()), '', ctx()->getMessage());
ctx()->setMessage('机器人' . $msg);
Console::info(ctx()->getMessage());
}
return true;
@@ -117,15 +135,16 @@ class Hello
* @CQCommand(pattern="*从*到*的随机数")
* @return string
*/
public function randNum() {
public function randNum()
{
// 获取第一个数字类型的参数
$num1 = ctx()->getNumArg("请输入第一个数字");
$num1 = ctx()->getNumArg('请输入第一个数字');
// 获取第二个数字类型的参数
$num2 = ctx()->getNumArg("请输入第二个数字");
$num2 = ctx()->getNumArg('请输入第二个数字');
$a = min(intval($num1), intval($num2));
$b = max(intval($num1), intval($num2));
// 回复用户结果
return "随机数是:" . mt_rand($a, $b);
return '随机数是:' . mt_rand($a, $b);
}
/**
@@ -133,8 +152,9 @@ class Hello
* @RequestMapping("/httpTimer")
* @Middleware("timer")
*/
public function timer() {
return "This page is used as testing TimerMiddleware! Do not use it in production.";
public function timer()
{
return 'This page is used as testing TimerMiddleware! Do not use it in production.';
}
/**
@@ -142,8 +162,9 @@ class Hello
* @RequestMapping("/index")
* @RequestMapping("/")
*/
public function index() {
return "Hello Zhamao!";
public function index()
{
return 'Hello Zhamao!';
}
/**
@@ -152,8 +173,9 @@ class Hello
* @param $param
* @return string
*/
public function paramGet($param) {
return "Hello, " . $param["name"];
public function paramGet($param)
{
return 'Hello, ' . $param['name'];
}
/**
@@ -161,17 +183,18 @@ class Hello
* @OnOpenEvent("qq")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
public function onConnect(ConnectionObject $conn)
{
Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已连接!');
}
/**
* 在机器人断开连接后向终端输出信息
* @OnCloseEvent("qq")
* @param ConnectionObject $conn
*/
public function onDisconnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已断开连接!");
public function onDisconnect(ConnectionObject $conn)
{
Console::info('机器人 ' . $conn->getOption('connect_id') . ' 已断开连接!');
}
/**
@@ -179,7 +202,8 @@ class Hello
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
* @throws InterruptException
*/
public function onRequest() {
public function onRequest()
{
EventDispatcher::interrupt();
}
@@ -187,8 +211,9 @@ class Hello
* 框架会默认关闭未知的WebSocket链接因为这个绑定的事件你可以根据你自己的需求进行修改
* @OnOpenEvent("default")
*/
public function closeUnknownConn() {
Console::info("Unknown connection , I will close it.");
server()->close(ctx()->getConnection()->getFd());
public function closeUnknownConn()
{
Console::info('Unknown connection , I will close it.');
server()->disconnect(ctx()->getConnection()->getFd());
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Module\Middleware;
use Exception;
@@ -13,7 +15,6 @@ use ZM\Http\MiddlewareInterface;
/**
* Class TimerMiddleware
* 示例中间件:用于统计路由函数运行时间用的
* @package Module\Middleware
* @MiddlewareClass("timer")
*/
class TimerMiddleware implements MiddlewareInterface
@@ -22,9 +23,9 @@ class TimerMiddleware implements MiddlewareInterface
/**
* @HandleBefore()
* @return bool
*/
public function onBefore(): bool {
public function onBefore(): bool
{
$this->starttime = microtime(true);
return true;
}
@@ -32,17 +33,18 @@ class TimerMiddleware implements MiddlewareInterface
/**
* @HandleAfter()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
public function onAfter()
{
Console::info('Using ' . round((microtime(true) - $this->starttime) * 1000, 3) . ' ms.');
}
/**
* @HandleException(\Exception::class)
* @param Exception $e
* @throws Exception
*/
public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
public function onException(Exception $e)
{
Console::error('Using ' . round((microtime(true) - $this->starttime) * 1000, 3) . ' ms but an Exception occurred.');
throw $e;
}
}

View File

@@ -1,11 +1,9 @@
<?php /** @noinspection PhpUnused */
/** @noinspection PhpMissingReturnTypeInspection */
<?php
declare(strict_types=1);
namespace ZM\API;
use ZM\Console\Console;
use ZM\Entity\CQObject;
@@ -16,12 +14,13 @@ class CQ
* @param $qq
* @return string
*/
public static function at($qq) {
if (is_numeric($qq) || $qq === "all") {
return "[CQ:at,qq=" . $qq . "]";
public static function at($qq)
{
if (is_numeric($qq) || $qq === 'all') {
return '[CQ:at,qq=' . $qq . ']';
}
Console::warning("传入的QQ号码($qq)错误!");
return " ";
Console::warning(zm_internal_errcode('E00035') . "传入的QQ号码({$qq})错误!");
return ' ';
}
/**
@@ -29,127 +28,130 @@ class CQ
* @param $id
* @return string
*/
public static function face($id) {
public static function face($id)
{
if (is_numeric($id)) {
return "[CQ:face,id=" . $id . "]";
return '[CQ:face,id=' . $id . ']';
}
Console::warning("传入的face id($id)错误!");
return " ";
Console::warning(zm_internal_errcode('E00035') . "传入的face id({$id})错误!");
return ' ';
}
/**
* 发送图片
* @param $file
* @param bool $cache
* @param bool $flash
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1) {
public static function image($file, bool $cache = true, bool $flash = false, bool $proxy = true, int $timeout = -1)
{
return
"[CQ:image,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
($flash ? ",type=flash" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
'[CQ:image,file=' . self::encode($file, true) .
(!$cache ? ',cache=0' : '') .
($flash ? ',type=flash' : '') .
(!$proxy ? ',proxy=false' : '') .
($timeout != -1 ? (',timeout=' . $timeout) : '') .
']';
}
/**
* 发送语音
* @param $file
* @param bool $magic
* @param bool $cache
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1) {
public static function record($file, bool $magic = false, bool $cache = true, bool $proxy = true, int $timeout = -1)
{
return
"[CQ:record,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
($magic ? ",magic=1" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
'[CQ:record,file=' . self::encode($file, true) .
(!$cache ? ',cache=0' : '') .
($magic ? ',magic=1' : '') .
(!$proxy ? ',proxy=false' : '') .
($timeout != -1 ? (',timeout=' . $timeout) : '') .
']';
}
/**
* 发送短视频
* @param $file
* @param bool $cache
* @param bool $proxy
* @param int $timeout
* @return string
*/
public static function video($file, $cache = true, $proxy = true, $timeout = -1) {
public static function video($file, bool $cache = true, bool $proxy = true, int $timeout = -1)
{
return
"[CQ:video,file=" . self::encode($file, true) .
(!$cache ? ",cache=0" : "") .
(!$proxy ? ",proxy=false" : "") .
($timeout != -1 ? (",timeout=" . intval($timeout)) : "") .
"]";
'[CQ:video,file=' . self::encode($file, true) .
(!$cache ? ',cache=0' : '') .
(!$proxy ? ',proxy=false' : '') .
($timeout != -1 ? (',timeout=' . $timeout) : '') .
']';
}
/**
* 发送投掷骰子(只能在单条回复中单独使用)
* @return string
*/
public static function rps() {
return "[CQ:rps]";
public static function rps()
{
return '[CQ:rps]';
}
/**
* 发送掷骰子表情(只能在单条回复中单独使用)
* @return string
*/
public static function dice() {
return "[CQ:dice]";
public static function dice()
{
return '[CQ:dice]';
}
/**
* 戳一戳(原窗口抖动,仅支持好友消息使用)
* @return string
*/
public static function shake() {
return "[CQ:shake]";
public static function shake()
{
return '[CQ:shake]';
}
/**
* 发送新的戳一戳
* @param $type
* @param $id
* @param string $name
* @return string
*/
public static function poke($type, $id, $name = "") {
return "[CQ:poke,type=$type,id=$id" . ($name != "" ? (",name=".self::encode($name, true)) : "") . "]";
public static function poke($type, $id, string $name = '')
{
return "[CQ:poke,type={$type},id={$id}" . ($name != '' ? (',name=' . self::encode($name, true)) : '') . ']';
}
/**
* 发送匿名消息
* @param int $ignore
* @return string
*/
public static function anonymous($ignore = 1) {
return "[CQ:anonymous" . ($ignore != 1 ? ",ignore=0" : "") . "]";
public static function anonymous(int $ignore = 1)
{
return '[CQ:anonymous' . ($ignore != 1 ? ',ignore=0' : '') . ']';
}
/**
* 发送链接分享(只能在单条回复中单独使用)
* @param $url
* @param $title
* @param null $content
* @param null $image
* @param null $content
* @param null $image
* @return string
*/
public static function share($url, $title, $content = null, $image = null) {
if ($content === null) $c = "";
else $c = ",content=" . self::encode($content, true);
if ($image === null) $i = "";
else $i = ",image=" . self::encode($image, true);
return "[CQ:share,url=" . self::encode($url, true) . ",title=" . self::encode($title, true) . $c . $i . "]";
public static function share($url, $title, $content = null, $image = null)
{
if ($content === null) {
$c = '';
} else {
$c = ',content=' . self::encode($content, true);
}
if ($image === null) {
$i = '';
} else {
$i = ',image=' . self::encode($image, true);
}
return '[CQ:share,url=' . self::encode($url, true) . ',title=' . self::encode($title, true) . $c . $i . ']';
}
/**
@@ -158,25 +160,25 @@ class CQ
* @param $id
* @return string
*/
public static function contact($type, $id) {
return "[CQ:contact,type=$type,id=$id]";
public static function contact($type, $id)
{
return "[CQ:contact,type={$type},id={$id}]";
}
/**
* 发送位置
* @param $lat
* @param $lon
* @param string $title
* @param string $content
* @return string
*/
public static function location($lat, $lon, $title = "", $content = "") {
return "[CQ:location" .
",lat=".self::encode($lat, true) .
",lon=".self::encode($lon, true).
($title != "" ? (",title=".self::encode($title, true)) : "") .
($content != "" ? (",content=".self::encode($content, true)) : "") .
"]";
public static function location($lat, $lon, string $title = '', string $content = '')
{
return '[CQ:location' .
',lat=' . self::encode($lat, true) .
',lon=' . self::encode($lon, true) .
($title != '' ? (',title=' . self::encode($title, true)) : '') .
($content != '' ? (',content=' . self::encode($content, true)) : '') .
']';
}
/**
@@ -191,101 +193,119 @@ class CQ
* $image 为音乐卡片的图片链接地址(可忽略)
* @param $type
* @param $id_or_url
* @param null $audio
* @param null $title
* @param null $content
* @param null $image
* @param null $audio
* @param null $title
* @param null $content
* @param null $image
* @return string
*/
public static function music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null) {
public static function music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null)
{
switch ($type) {
case "qq":
case "163":
case "xiami":
return "[CQ:music,type=$type,id=$id_or_url]";
case "custom":
case 'qq':
case '163':
case 'xiami':
return "[CQ:music,type={$type},id={$id_or_url}]";
case 'custom':
if ($title === null || $audio === null) {
Console::warning("传入CQ码实例的标题和音频链接不能为空");
return " ";
Console::warning(zm_internal_errcode('E00035') . '传入CQ码实例的标题和音频链接不能为空');
return ' ';
}
if ($content === null) $c = "";
else $c = ",content=" . self::encode($content, true);
if ($image === null) $i = "";
else $i = ",image=" . self::encode($image, true);
return "[CQ:music,type=custom,url=" .
if ($content === null) {
$c = '';
} else {
$c = ',content=' . self::encode($content, true);
}
if ($image === null) {
$i = '';
} else {
$i = ',image=' . self::encode($image, true);
}
return '[CQ:music,type=custom,url=' .
self::encode($id_or_url, true) .
",audio=" . self::encode($audio, true) . ",title=" . self::encode($title, true) . $c . $i .
"]";
',audio=' . self::encode($audio, true) . ',title=' . self::encode($title, true) . $c . $i .
']';
default:
Console::warning("传入的music type($type)错误!");
return " ";
Console::warning(zm_internal_errcode('E00035') . "传入的music type({$type})错误!");
return ' ';
}
}
public static function forward($id) {
return "[CQ:forward,id=$id]";
public static function forward($id)
{
return '[CQ:forward,id=' . self::encode($id) . ']';
}
public static function node($user_id, $nickname, $content) {
return "[CQ:node,user_id=$user_id,nickname=".self::encode($nickname, true).",content=" . self::encode($content, true) . "]";
public static function node($user_id, $nickname, $content)
{
return "[CQ:node,user_id={$user_id},nickname=" . self::encode($nickname, true) . ',content=' . self::encode($content, true) . ']';
}
public static function xml($data) {
return "[CQ:xml,data=" . self::encode($data, true) . "]";
public static function xml($data)
{
return '[CQ:xml,data=' . self::encode($data, true) . ']';
}
public static function json($data, $resid = 0) {
return "[CQ:json,data=" . self::encode($data, true) . ",resid=" . intval($resid) . "]";
public static function json($data, $resid = 0)
{
return '[CQ:json,data=' . self::encode($data, true) . ',resid=' . intval($resid) . ']';
}
public static function _custom(string $type_name, $params) {
$code = "[CQ:" . $type_name;
public static function _custom(string $type_name, $params)
{
$code = '[CQ:' . $type_name;
foreach ($params as $k => $v) {
$code .= "," . $k . "=" . self::escape($v, true);
$code .= ',' . $k . '=' . self::escape($v, true);
}
$code .= "]";
$code .= ']';
return $code;
}
/**
* 反转义字符串中的CQ码敏感符号
* @param $msg
* @param bool $is_content
* @return mixed
* @param mixed $msg
* @param mixed $is_content
*/
public static function decode($msg, $is_content = false) {
$msg = str_replace(["&amp;", "&#91;", "&#93;"], ["&", "[", "]"], $msg);
if ($is_content) $msg = str_replace("&#44;", ",", $msg);
public static function decode($msg, $is_content = false)
{
$msg = str_replace(['&amp;', '&#91;', '&#93;'], ['&', '[', ']'], $msg);
if ($is_content) {
$msg = str_replace('&#44;', ',', $msg);
}
return $msg;
}
public static function replace($str) {
$str = str_replace("{{", "[", $str);
$str = str_replace("}}", "]", $str);
return $str;
public static function replace($str)
{
$str = str_replace('{{', '[', $str);
return str_replace('}}', ']', $str);
}
/**
* 转义CQ码的特殊字符同encode
* @param $msg
* @param bool $is_content
* @return mixed
* @param mixed $msg
* @param mixed $is_content
*/
public static function escape($msg, $is_content = false) {
$msg = str_replace(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $msg);
public static function escape($msg, $is_content = false)
{
$msg = str_replace(['&', '[', ']'], ['&amp;', '&#91;', '&#93;'], $msg);
if ($is_content) {
$msg = str_replace(',', '&#44;', $msg);
}
return $msg;
}
/**
* 转义CQ码的特殊字符
* @param $msg
* @param false $is_content
* @return mixed
* @param mixed $msg
* @param mixed $is_content
*/
public static function encode($msg, $is_content = false) {
$msg = str_replace(["&", "[", "]"], ["&amp;", "&#91;", "&#93;"], $msg);
if ($is_content) $msg = str_replace(",", "&#44;", $msg);
public static function encode($msg, $is_content = false)
{
$msg = str_replace(['&', '[', ']'], ['&amp;', '&#91;', '&#93;'], $msg);
if ($is_content) {
$msg = str_replace(',', '&#44;', $msg);
}
return $msg;
}
@@ -294,12 +314,13 @@ class CQ
* @param $msg
* @return string
*/
public static function removeCQ($msg) {
$final = "";
public static function removeCQ($msg)
{
$final = '';
$last_end = 0;
foreach(self::getAllCQ($msg) as $k => $v) {
$final .= mb_substr($msg, $last_end, $v["start"] - $last_end);
$last_end = $v["end"] + 1;
foreach (self::getAllCQ($msg) as $v) {
$final .= mb_substr($msg, $last_end, $v['start'] - $last_end);
$last_end = $v['end'] + 1;
}
$final .= mb_substr($msg, $last_end);
return $final;
@@ -307,56 +328,59 @@ class CQ
/**
* 获取消息中第一个CQ码
* @param $msg
* @param bool $is_object
* @return array|CQObject|null
* @param mixed $msg
* @param mixed $is_object
*/
public static function getCQ($msg, $is_object = false) {
if (($head = mb_strpos($msg, "[CQ:")) !== false) {
public static function getCQ($msg, $is_object = false)
{
if (($head = mb_strpos($msg, '[CQ:')) !== false) {
$key_offset = mb_substr($msg, $head);
$close = mb_strpos($key_offset, "]");
if ($close === false) return null;
$content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg));
$exp = explode(",", $content);
$cq["type"] = array_shift($exp);
foreach ($exp as $k => $v) {
$ss = explode("=", $v);
$sk = array_shift($ss);
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
$close = mb_strpos($key_offset, ']');
if ($close === false) {
return null;
}
$cq["start"] = $head;
$cq["end"] = $close + $head;
$content = mb_substr($msg, $head + 4, $close + $head - mb_strlen($msg));
$exp = explode(',', $content);
$cq['type'] = array_shift($exp);
foreach ($exp as $v) {
$ss = explode('=', $v);
$sk = array_shift($ss);
$cq['params'][$sk] = self::decode(implode('=', $ss), true);
}
$cq['start'] = $head;
$cq['end'] = $close + $head;
return !$is_object ? $cq : CQObject::fromArray($cq);
} else {
return null;
}
return null;
}
/**
* 获取消息中所有的CQ码
* @param $msg
* @param bool $is_object
* @return array|CQObject[]
* @param mixed $msg
* @param mixed $is_object
*/
public static function getAllCQ($msg, $is_object = false) {
public static function getAllCQ($msg, $is_object = false)
{
$cqs = [];
$offset = 0;
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), "[CQ:")) !== false) {
while (($head = mb_strpos(($submsg = mb_substr($msg, $offset)), '[CQ:')) !== false) {
$key_offset = mb_substr($submsg, $head);
$tmpmsg = mb_strpos($key_offset, "]");
if ($tmpmsg === false) break; // 没闭合不算CQ码
$tmpmsg = mb_strpos($key_offset, ']');
if ($tmpmsg === false) {
break;
} // 没闭合不算CQ码
$content = mb_substr($submsg, $head + 4, $tmpmsg + $head - mb_strlen($submsg));
$exp = explode(",", $content);
$exp = explode(',', $content);
$cq = [];
$cq["type"] = array_shift($exp);
foreach ($exp as $k => $v) {
$ss = explode("=", $v);
$cq['type'] = array_shift($exp);
foreach ($exp as $v) {
$ss = explode('=', $v);
$sk = array_shift($ss);
$cq["params"][$sk] = self::decode(implode("=", $ss), true);
$cq['params'][$sk] = self::decode(implode('=', $ss), true);
}
$cq["start"] = $offset + $head;
$cq["end"] = $offset + $tmpmsg + $head;
$offset += $tmpmsg + 1;
$cq['start'] = $offset + $head;
$cq['end'] = $offset + $tmpmsg + $head;
$offset += $head + $tmpmsg + 1;
$cqs[] = (!$is_object ? $cq : CQObject::fromArray($cq));
}
return $cqs;

View File

@@ -1,8 +1,10 @@
<?php
declare(strict_types=1);
namespace ZM\API;
use Closure;
use ZM\Console\Console;
use ZM\Store\LightCacheInside;
use ZM\Store\Lock\SpinLock;
@@ -11,66 +13,90 @@ use ZM\Utils\CoMessage;
trait CQAPI
{
/** @var null|Closure */
private static $filter;
public function __call($name, $arguments)
{
return false;
}
public static function registerFilter(callable $callable)
{
self::$filter = $callable;
}
public function getActionName($suffix, string $method)
{
$postfix = ($suffix == OneBotV11::API_ASYNC ? '_async' : ($suffix == OneBotV11::API_RATE_LIMITED ? '_rate_limited' : ''));
$func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method));
return $func_name . $postfix;
}
/**
* @param $connection
* @param $reply
* @param |null $function
* @return bool|array
* @return array|bool
*/
private function processAPI($connection, $reply, $function = null) {
if ($connection->getOption("type") === CONN_WEBSOCKET)
private function processAPI($connection, $reply, $function = null)
{
if (is_callable(self::$filter)) {
$reply2 = call_user_func(self::$filter, $reply);
if (is_bool($reply2)) {
return $reply2;
}
$reply = $reply2;
}
if ($connection->getOption('type') === CONN_WEBSOCKET) {
return $this->processWebsocketAPI($connection, $reply, $function);
else
return $this->processHttpAPI($connection, $reply, $function);
}
return $this->processHttpAPI($connection, $reply, $function);
}
public function processWebsocketAPI($connection, $reply, $function = false) {
$api_id = ZMAtomic::get("wait_msg_id")->add(1);
$reply["echo"] = $api_id;
private function processWebsocketAPI($connection, $reply, $function = false)
{
$api_id = ZMAtomic::get('wait_msg_id')->add(1);
$reply['echo'] = $api_id;
if (server()->push($connection->getFd(), json_encode($reply))) {
if ($function === true) {
$obj = [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getOption("connect_id"),
"echo" => $api_id
'data' => $reply,
'time' => microtime(true),
'self_id' => $connection->getOption('connect_id'),
'echo' => $api_id,
];
return CoMessage::yieldByWS($obj, ["echo"], 60);
return CoMessage::yieldByWS($obj, ['echo'], 30);
}
return true;
} else {
Console::warning("CQAPI send failed, websocket push error.");
$response = [
"status" => "failed",
"retcode" => -1000,
"data" => null,
"self_id" => $connection->getOption("connect_id")
];
SpinLock::lock("wait_api");
$r = LightCacheInside::get("wait_api", "wait_api");
unset($r[$reply["echo"]]);
LightCacheInside::set("wait_api", "wait_api", $r);
SpinLock::unlock("wait_api");
if ($function === true) return $response;
return false;
}
Console::warning(zm_internal_errcode('E00036') . 'CQAPI send failed, websocket push error.');
$response = [
'status' => 'failed',
'retcode' => -1000,
'data' => null,
'self_id' => $connection->getOption('connect_id'),
];
SpinLock::lock('wait_api');
$r = LightCacheInside::get('wait_api', 'wait_api');
unset($r[$reply['echo']]);
LightCacheInside::set('wait_api', 'wait_api', $r);
SpinLock::unlock('wait_api');
if ($function === true) {
return $response;
}
return false;
}
/**
* @param $connection
* @param $reply
* @param null $function
* @return bool
* @noinspection PhpUnusedParameterInspection
*/
public function processHttpAPI($connection, $reply, $function = null): bool {
return false;
}
/** @noinspection PhpMissingReturnTypeInspection */
public function __call($name, $arguments) {
private function processHttpAPI($connection, $reply, $function = null): bool
{
return false;
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace ZM\API;
class GoCqhttpAPIV11
{
use CQAPI;
public const SUPPORT_VERSION = '1.0.0-beta8';
private $connection;
private $callback;
private $prefix;
public function __construct($connection, $callback, $prefix)
{
$this->connection = $connection;
$this->callback = $callback;
$this->prefix = $prefix;
}
/**
* 获取频道系统内BOT的资料
* 响应字段nickname, tiny_id, avatar_url
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E8%8E%B7%E5%8F%96%E9%A2%91%E9%81%93%E7%B3%BB%E7%BB%9F%E5%86%85bot%E7%9A%84%E8%B5%84%E6%96%99
* @return array|bool
*/
public function getGuildServiceProfile()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取频道列表
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E8%8E%B7%E5%8F%96%E9%A2%91%E9%81%93%E5%88%97%E8%A1%A8
* @return array|bool
*/
public function getGuildList()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 通过访客获取频道元数据
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E9%80%9A%E8%BF%87%E8%AE%BF%E5%AE%A2%E8%8E%B7%E5%8F%96%E9%A2%91%E9%81%93%E5%85%83%E6%95%B0%E6%8D%AE
* @param $guild_id
* @return array|bool
*/
public function getGuildMetaByGuest($guild_id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'guild_id' => $guild_id,
],
], $this->callback);
}
/**
* 获取子频道列表
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E8%8E%B7%E5%8F%96%E5%AD%90%E9%A2%91%E9%81%93%E5%88%97%E8%A1%A8
* @param $guild_id
* @param false $no_cache
* @return array|bool
*/
public function getGuildChannelList($guild_id, bool $no_cache = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'guild_id' => $guild_id,
'no_cache' => $no_cache,
],
], $this->callback);
}
/**
* 获取频道成员列表
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E8%8E%B7%E5%8F%96%E9%A2%91%E9%81%93%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
* @param $guild_id
* @return array|bool
*/
public function getGuildMembers($guild_id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'guild_id' => $guild_id,
],
], $this->callback);
}
/**
* 发送信息到子频道
* @see https://github.com/Mrs4s/go-cqhttp/blob/master/docs/guild.md#%E5%8F%91%E9%80%81%E4%BF%A1%E6%81%AF%E5%88%B0%E5%AD%90%E9%A2%91%E9%81%93
* @param $guild_id
* @param $channel_id
* @param $message
* @return array|bool
*/
public function sendGuildChannelMsg($guild_id, $channel_id, $message)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'guild_id' => $guild_id,
'channel_id' => $channel_id,
'message' => $message,
],
], $this->callback);
}
}

729
src/ZM/API/OneBotV11.php Normal file
View File

@@ -0,0 +1,729 @@
<?php
/** @noinspection PhpUnused */
declare(strict_types=1);
namespace ZM\API;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Exception\RobotNotFoundException;
use ZM\Exception\ZMKnownException;
/**
* OneBot V11 的 API 实现类
* Class OneBotV11
* @since 2.5.0
*/
class OneBotV11
{
use CQAPI;
public const API_ASYNC = 1;
public const API_NORMAL = 0;
public const API_RATE_LIMITED = 2;
/** @var null|ConnectionObject */
protected $connection;
protected $callback = true;
protected $prefix = 0;
public function __construct(ConnectionObject $connection)
{
$this->connection = $connection;
}
/**
* @param $robot_id
* @throws RobotNotFoundException
* @return ZMRobot
*/
public static function get($robot_id)
{
$r = ManagerGM::getAllByName('qq');
foreach ($r as $v) {
if ($v->getOption('connect_id') == $robot_id) {
return new ZMRobot($v);
}
}
throw new RobotNotFoundException('机器人 ' . $robot_id . ' 未连接到框架!');
}
/**
* @throws RobotNotFoundException
* @return ZMRobot
*/
public static function getRandom()
{
$r = ManagerGM::getAllByName('qq');
if ($r == []) {
throw new RobotNotFoundException('没有任何机器人连接到框架!');
}
return new ZMRobot($r[array_rand($r)]);
}
/**
* @return ZMRobot[]
*/
public static function getAllRobot()
{
$r = ManagerGM::getAllByName('qq');
$obj = [];
foreach ($r as $v) {
$obj[] = new ZMRobot($v);
}
return $obj;
}
public function setCallback($callback = true)
{
$this->callback = $callback;
return $this;
}
public function setPrefix($prefix = self::API_NORMAL)
{
$this->prefix = $prefix;
return $this;
}
public function getSelfId()
{
return $this->connection->getOption('connect_id');
}
/* 下面是 OneBot 标准的 V11 公开 API */
/**
* 发送私聊消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
* @param $user_id
* @param $message
* @return null|array|bool
*/
public function sendPrivateMsg($user_id, $message, bool $auto_escape = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'user_id' => $user_id,
'message' => $message,
'auto_escape' => $auto_escape,
],
], $this->callback);
}
/**
* 发送群消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
* @param $group_id
* @param $message
* @return null|array|bool
*/
public function sendGroupMsg($group_id, $message, bool $auto_escape = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'message' => $message,
'auto_escape' => $auto_escape,
],
], $this->callback);
}
/**
* 发送消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
* @param $message_type
* @param $target_id
* @param $message
* @return null|array|bool
*/
public function sendMsg($message_type, $target_id, $message, bool $auto_escape = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'message_type' => $message_type,
($message_type == 'private' ? 'user' : $message_type) . '_id' => $target_id,
'message' => $message,
'auto_escape' => $auto_escape,
],
], $this->callback);
}
/**
* 撤回消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
* @param $message_id
* @return null|array|bool
*/
public function deleteMsg($message_id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'message_id' => $message_id,
],
], $this->callback);
}
/**
* 获取消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_msg-%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
* @param $message_id
* @return null|array|bool
*/
public function getMsg($message_id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'message_id' => $message_id,
],
], $this->callback);
}
/**
* 获取合并转发消息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF
* @param $id
* @return null|array|bool
*/
public function getForwardMsg($id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'id' => $id,
],
], $this->callback);
}
/**
* 发送好友赞
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_like-%E5%8F%91%E9%80%81%E5%A5%BD%E5%8F%8B%E8%B5%9E
* @param $user_id
* @return null|array|bool
*/
public function sendLike($user_id, int $times = 1)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'user_id' => $user_id,
'times' => $times,
],
], $this->callback);
}
/**
* 群组踢人
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function setGroupKick($group_id, $user_id, bool $reject_add_request = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'reject_add_request' => $reject_add_request,
],
], $this->callback);
}
/**
* 群组单人禁言
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function setGroupBan($group_id, $user_id, int $duration = 1800)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'duration' => $duration,
],
], $this->callback);
}
/**
* 群组匿名用户禁言
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous_ban-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D%E7%94%A8%E6%88%B7%E7%A6%81%E8%A8%80
* @param $group_id
* @param $anonymous_or_flag
* @return null|array|bool
*/
public function setGroupAnonymousBan($group_id, $anonymous_or_flag, int $duration = 1800)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
(is_string($anonymous_or_flag) ? 'flag' : 'anonymous') => $anonymous_or_flag,
'duration' => $duration,
],
], $this->callback);
}
/**
* 群组全员禁言
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
* @param $group_id
* @return null|array|bool
*/
public function setGroupWholeBan($group_id, bool $enable = true)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable,
],
], $this->callback);
}
/**
* 群组设置管理员
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function setGroupAdmin($group_id, $user_id, bool $enable = true)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'enable' => $enable,
],
], $this->callback);
}
/**
* 群组匿名
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D
* @param $group_id
* @return null|array|bool
*/
public function setGroupAnonymous($group_id, bool $enable = true)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable,
],
], $this->callback);
}
/**
* 设置群名片(群备注)
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function setGroupCard($group_id, $user_id, string $card = '')
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'card' => $card,
],
], $this->callback);
}
/**
* 设置群名
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
* @param $group_id
* @param $group_name
* @return null|array|bool
*/
public function setGroupName($group_id, $group_name)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'group_name' => $group_name,
],
], $this->callback);
}
/**
* 退出群组
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
* @param $group_id
* @return null|array|bool
*/
public function setGroupLeave($group_id, bool $is_dismiss = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'is_dismiss' => $is_dismiss,
],
], $this->callback);
}
/**
* 设置群组专属头衔
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function setGroupSpecialTitle($group_id, $user_id, string $special_title = '', int $duration = -1)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'special_title' => $special_title,
'duration' => $duration,
],
], $this->callback);
}
/**
* 处理加好友请求
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
* @param $flag
* @return null|array|bool
*/
public function setFriendAddRequest($flag, bool $approve = true, string $remark = '')
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'flag' => $flag,
'approve' => $approve,
'remark' => $remark,
],
], $this->callback);
}
/**
* 处理加群请求/邀请
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
* @param $flag
* @param $sub_type
* @return null|array|bool
*/
public function setGroupAddRequest($flag, $sub_type, bool $approve = true, string $reason = '')
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'flag' => $flag,
'sub_type' => $sub_type,
'approve' => $approve,
'reason' => $reason,
],
], $this->callback);
}
/**
* 获取登录号信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
* @return null|array|bool
*/
public function getLoginInfo()
{
return $this->processAPI($this->connection, ['action' => $this->getActionName($this->prefix, __FUNCTION__)], $this->callback);
}
/**
* 获取陌生人信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
* @param $user_id
* @return null|array|bool
*/
public function getStrangerInfo($user_id, bool $no_cache = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'user_id' => $user_id,
'no_cache' => $no_cache,
],
], $this->callback);
}
/**
* 获取好友列表
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
* @return null|array|bool
*/
public function getFriendList()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取群信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
* @param $group_id
* @return null|array|bool
*/
public function getGroupInfo($group_id, bool $no_cache = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'no_cache' => $no_cache,
],
], $this->callback);
}
/**
* 获取群列表
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
* @return null|array|bool
*/
public function getGroupList()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取群成员信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $user_id
* @return null|array|bool
*/
public function getGroupMemberInfo($group_id, $user_id, bool $no_cache = false)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'no_cache' => $no_cache,
],
], $this->callback);
}
/**
* 获取群成员列表
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
* @param $group_id
* @return null|array|bool
*/
public function getGroupMemberList($group_id)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
],
], $this->callback);
}
/**
* 获取群荣誉信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $type
* @return null|array|bool
*/
public function getGroupHonorInfo($group_id, $type)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'group_id' => $group_id,
'type' => $type,
],
], $this->callback);
}
/**
* 获取 CSRF Token
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_csrf_token-%E8%8E%B7%E5%8F%96-csrf-token
* @return null|array|bool
*/
public function getCsrfToken()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取 QQ 相关接口凭证
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_credentials-%E8%8E%B7%E5%8F%96-qq-%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3%E5%87%AD%E8%AF%81
* @return null|array|bool
*/
public function getCredentials(string $domain = '')
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'domain' => $domain,
],
], $this->callback);
}
/**
* 获取语音
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_record-%E8%8E%B7%E5%8F%96%E8%AF%AD%E9%9F%B3
* @param $file
* @param $out_format
* @return null|array|bool
*/
public function getRecord($file, $out_format)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'file' => $file,
'out_format' => $out_format,
],
], $this->callback);
}
/**
* 获取图片
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_image-%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87
* @param $file
* @return null|array|bool
*/
public function getImage($file)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'file' => $file,
],
], $this->callback);
}
/**
* 检查是否可以发送图片
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
* @return null|array|bool
*/
public function canSendImage()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 检查是否可以发送语音
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
* @return null|array|bool
*/
public function canSendRecord()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取运行状态
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_status-%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81
* @return null|array|bool
*/
public function getStatus()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取版本信息
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
* @return null|array|bool
*/
public function getVersionInfo()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 重启 OneBot 实现
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
* @return null|array|bool
*/
public function setRestart(int $delay = 0)
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
'params' => [
'delay' => $delay,
],
], $this->callback);
}
/**
* 清理缓存
* @see https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#clean_cache-%E6%B8%85%E7%90%86%E7%BC%93%E5%AD%98
* @return null|array|bool
*/
public function cleanCache()
{
return $this->processAPI($this->connection, [
'action' => $this->getActionName($this->prefix, __FUNCTION__),
], $this->callback);
}
/**
* 获取内置支持的扩展API对象
* 现支持 go-cqhttp 的扩展API
* @throws ZMKnownException
* @return mixed
*/
public function getExtendedAPI(string $package_name = 'go-cqhttp')
{
$table = [
'go-cqhttp' => GoCqhttpAPIV11::class,
];
if (isset($table[$package_name])) {
return new $table[$package_name]($this->connection, $this->callback, $this->prefix);
}
throw new ZMKnownException(zm_internal_errcode('E00071'), '无法找到对应的调用扩展类');
}
public function callExtendedAPI($action, $params = [])
{
return $this->processAPI($this->connection, [
'action' => $action,
'params' => $params,
], $this->callback);
}
}

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace ZM\API;
use Swoole\Coroutine\Http\Client;
use ZM\Console\Console;
@@ -16,103 +16,113 @@ class TuringAPI
* @param $api
* @return string
*/
public static function getTuringMsg($msg, $user_id, $api) {
public static function getTuringMsg($msg, $user_id, $api)
{
$origin = $msg;
if (($cq = CQ::getCQ($msg)) !== null) {//如有CQ码则去除
if ($cq["type"] == "image") {
$url = $cq["params"]["url"];
$msg = str_replace(mb_substr($msg, $cq["start"], $cq["end"] - $cq["start"] + 1), "", $msg);
if (($cq = CQ::getCQ($msg)) !== null) {// 如有CQ码则去除
if ($cq['type'] == 'image') {
$url = $cq['params']['url'];
$msg = str_replace(mb_substr($msg, $cq['start'], $cq['end'] - $cq['start'] + 1), '', $msg);
}
$msg = trim($msg);
}
//构建将要发送的json包给图灵
// 构建将要发送的json包给图灵
$content = [
"reqType" => 0,
"userInfo" => [
"apiKey" => $api,
"userId" => $user_id
]
'reqType' => 0,
'userInfo' => [
'apiKey' => $api,
'userId' => $user_id,
],
];
if ($msg != "") {
$content["perception"]["inputText"]["text"] = $msg;
if ($msg != '') {
$content['perception']['inputText']['text'] = $msg;
}
$msg = trim($msg);
if (mb_strlen($msg) < 1 && !isset($url)) return "请说出你想说的话";
if (isset($url)) {
$content["perception"]["inputImage"]["url"] = $url;
$content["reqType"] = 1;
if (mb_strlen($msg) < 1 && !isset($url)) {
return '请说出你想说的话';
}
if (!isset($content["perception"])) return "请说出你想说的话";
$client = new Client("openapi.tuling123.com", 80);
$client->setHeaders(["Content-type" => "application/json"]);
$client->post("/openapi/api/v2", json_encode($content, JSON_UNESCAPED_UNICODE));
if (isset($url)) {
$content['perception']['inputImage']['url'] = $url;
$content['reqType'] = 1;
}
if (!isset($content['perception'])) {
return '请说出你想说的话';
}
$client = new Client('openapi.tuling123.com', 80);
$client->setHeaders(['Content-type' => 'application/json']);
$client->post('/openapi/api/v2', json_encode($content, JSON_UNESCAPED_UNICODE));
$api_return = json_decode($client->body, true);
if (!isset($api_return["intent"]["code"])) return "XD 哎呀,我脑子突然短路了,请稍后再问我吧!";
if (!isset($api_return['intent']['code'])) {
return 'XD 哎呀,我脑子突然短路了,请稍后再问我吧!';
}
$status = self::getResultStatus($api_return);
if ($status !== true) {
if ($status == "err:输入文本内容超长(上限150)") return "你的话太多了!!!";
if ($api_return["intent"]["code"] == 4003) {
return "哎呀,我刚才有点走神了,可能忘记你说什么了,可以重说一遍吗";
if ($status == 'err:输入文本内容超长(上限150)') {
return '你的话太多了!!!';
}
Console::error("图灵机器人发送错误!\n错误原始内容:" . $origin . "\n来自:" . $user_id . "\n错误信息:" . $status);
//echo json_encode($r, 128|256);
return "哎呀,我刚才有点走神了,要不一会儿换一种问题试试?";
if ($api_return['intent']['code'] == 4003) {
return '哎呀,我刚才有点走神了,可能忘记你说什么了,可以重说一遍吗';
}
Console::error(zm_internal_errcode('E00038') . "图灵机器人发送错误!\n错误原始内容:" . $origin . "\n来自:" . $user_id . "\n错误信息:" . $status);
// echo json_encode($r, 128|256);
return '哎呀,我刚才有点走神了,要不一会儿换一种问题试试?';
}
$result = $api_return["results"];
//Console::info(Console::setColor(json_encode($result, 128 | 256), "green"));
$final = "";
foreach ($result as $k => $v) {
switch ($v["resultType"]) {
case "url":
$final .= "\n" . $v["values"]["url"];
$result = $api_return['results'];
// Console::info(Console::setColor(json_encode($result, 128 | 256), "green"));
$final = '';
foreach ($result as $v) {
switch ($v['resultType']) {
case 'url':
$final .= "\n" . $v['values']['url'];
break;
case "text":
$final .= "\n" . $v["values"]["text"];
case 'text':
$final .= "\n" . $v['values']['text'];
break;
case "image":
$final .= "\n" . CQ::image($v["values"]["image"]);
case 'image':
$final .= "\n" . CQ::image($v['values']['image']);
break;
}
}
return trim($final);
}
public static function getResultStatus($r) {
switch ($r["intent"]["code"]) {
public static function getResultStatus($r)
{
switch ($r['intent']['code']) {
case 5000:
return "err:无解析结果";
return 'err:无解析结果';
case 4000:
case 6000:
return "err:暂不支持该功能";
return 'err:暂不支持该功能';
case 4001:
return "err:加密方式错误";
return 'err:加密方式错误';
case 4005:
case 4002:
return "err:无功能权限";
return 'err:无功能权限';
case 4003:
return "err:该apikey没有可用请求次数";
return 'err:该apikey没有可用请求次数';
case 4007:
return "err:apikey不合法";
return 'err:apikey不合法';
case 4100:
return "err:userid获取失败";
return 'err:userid获取失败';
case 4200:
return "err:上传格式错误";
return 'err:上传格式错误';
case 4300:
return "err:批量操作超过限制";
return 'err:批量操作超过限制';
case 4400:
return "err:没有上传合法userid";
return 'err:没有上传合法userid';
case 4500:
return "err:userid申请个数超过限制";
return 'err:userid申请个数超过限制';
case 4600:
return "err:输入内容为空";
return 'err:输入内容为空';
case 4602:
return "err:输入文本内容超长(上限150)";
return 'err:输入文本内容超长(上限150)';
case 7002:
return "err:上传信息失败";
return 'err:上传信息失败';
case 8008:
return "err:服务器错误";
return 'err:服务器错误';
default:
return true;
}
}
}
}

View File

@@ -1,691 +1,14 @@
<?php /** @noinspection PhpMissingReturnTypeInspection */
/** @noinspection PhpUnused */
<?php
declare(strict_types=1);
namespace ZM\API;
use ZM\ConnectionManager\ConnectionObject;
use ZM\ConnectionManager\ManagerGM;
use ZM\Exception\RobotNotFoundException;
/**
* Class ZMRobot
* @package ZM\Utils
* @since 1.2
* @version V11
*/
class ZMRobot
class ZMRobot extends OneBotV11
{
use CQAPI;
const API_ASYNC = 1;
const API_NORMAL = 0;
const API_RATE_LIMITED = 2;
/** @var ConnectionObject|null */
private $connection;
private $callback = true;
private $prefix = 0;
/**
* @param $robot_id
* @return ZMRobot
* @throws RobotNotFoundException
*/
public static function get($robot_id) {
$r = ManagerGM::getAllByName('qq');
foreach ($r as $v) {
if ($v->getOption('connect_id') == $robot_id) return new ZMRobot($v);
}
throw new RobotNotFoundException("机器人 " . $robot_id . " 未连接到框架!");
}
/**
* @return ZMRobot
* @throws RobotNotFoundException
*/
public static function getRandom() {
$r = ManagerGM::getAllByName('qq');
if ($r == []) throw new RobotNotFoundException("没有任何机器人连接到框架!");
return new ZMRobot($r[array_rand($r)]);
}
public static function getFirst() {
}
/**
* @return ZMRobot[]
*/
public static function getAllRobot() {
$r = ManagerGM::getAllByName('qq');
$obj = [];
foreach ($r as $v) {
$obj[] = new ZMRobot($v);
}
return $obj;
}
public function __construct(ConnectionObject $connection) {
$this->connection = $connection;
}
public function setCallback($callback = true) {
$this->callback = $callback;
return $this;
}
public function setPrefix($prefix = self::API_NORMAL) {
$this->prefix = $prefix;
return $this;
}
public function getSelfId() {
return $this->connection->getOption('connect_id');
}
/* 下面是 OneBot 标准的 V11 公开 API */
/**
* 发送私聊消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_private_msg-%E5%8F%91%E9%80%81%E7%A7%81%E8%81%8A%E6%B6%88%E6%81%AF
* @param $user_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendPrivateMsg($user_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 发送群消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_group_msg-%E5%8F%91%E9%80%81%E7%BE%A4%E6%B6%88%E6%81%AF
* @param $group_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendGroupMsg($group_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 发送消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_msg-%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
* @param $message_type
* @param $target_id
* @param $message
* @param bool $auto_escape
* @return array|bool|null
*/
public function sendMsg($message_type, $target_id, $message, $auto_escape = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_type' => $message_type,
($message_type == 'private' ? 'user' : $message_type) . '_id' => $target_id,
'message' => $message,
'auto_escape' => $auto_escape
]
], $this->callback);
}
/**
* 撤回消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#delete_msg-%E6%92%A4%E5%9B%9E%E6%B6%88%E6%81%AF
* @param $message_id
* @return array|bool|null
*/
public function deleteMsg($message_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_id' => $message_id
]
], $this->callback);
}
/**
* 获取消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_msg-%E8%8E%B7%E5%8F%96%E6%B6%88%E6%81%AF
* @param $message_id
* @return array|bool|null
*/
public function getMsg($message_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'message_id' => $message_id
]
], $this->callback);
}
/**
* 获取合并转发消息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-%E8%8E%B7%E5%8F%96%E5%90%88%E5%B9%B6%E8%BD%AC%E5%8F%91%E6%B6%88%E6%81%AF
* @param $id
* @return array|bool|null
*/
public function getForwardMsg($id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'id' => $id
]
], $this->callback);
}
/**
* 发送好友赞
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#send_like-%E5%8F%91%E9%80%81%E5%A5%BD%E5%8F%8B%E8%B5%9E
* @param $user_id
* @param int $times
* @return array|bool|null
*/
public function sendLike($user_id, $times = 1) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'times' => $times
]
], $this->callback);
}
/**
* 群组踢人
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_kick-%E7%BE%A4%E7%BB%84%E8%B8%A2%E4%BA%BA
* @param $group_id
* @param $user_id
* @param bool $reject_add_request
* @return array|bool|null
*/
public function setGroupKick($group_id, $user_id, $reject_add_request = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'reject_add_request' => $reject_add_request
]
], $this->callback);
}
/**
* 群组单人禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_ban-%E7%BE%A4%E7%BB%84%E5%8D%95%E4%BA%BA%E7%A6%81%E8%A8%80
* @param $group_id
* @param $user_id
* @param $duration
* @return array|bool|null
*/
public function setGroupBan($group_id, $user_id, $duration = 1800) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'duration' => $duration
]
], $this->callback);
}
/**
* 群组匿名用户禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous_ban-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D%E7%94%A8%E6%88%B7%E7%A6%81%E8%A8%80
* @param $group_id
* @param $anonymous_or_flag
* @param int $duration
* @return array|bool|null
*/
public function setGroupAnonymousBan($group_id, $anonymous_or_flag, $duration = 1800) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
(is_string($anonymous_or_flag) ? 'flag' : 'anonymous') => $anonymous_or_flag,
'duration' => $duration
]
], $this->callback);
}
/**
* 群组全员禁言
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_whole_ban-%E7%BE%A4%E7%BB%84%E5%85%A8%E5%91%98%E7%A6%81%E8%A8%80
* @param $group_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupWholeBan($group_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 群组设置管理员
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_admin-%E7%BE%A4%E7%BB%84%E8%AE%BE%E7%BD%AE%E7%AE%A1%E7%90%86%E5%91%98
* @param $group_id
* @param $user_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupAdmin($group_id, $user_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 群组匿名
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_anonymous-%E7%BE%A4%E7%BB%84%E5%8C%BF%E5%90%8D
* @param $group_id
* @param bool $enable
* @return array|bool|null
*/
public function setGroupAnonymous($group_id, $enable = true) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'enable' => $enable
]
], $this->callback);
}
/**
* 设置群名片(群备注)
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_card-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D%E7%89%87%E7%BE%A4%E5%A4%87%E6%B3%A8
* @param $group_id
* @param $user_id
* @param string $card
* @return array|bool|null
*/
public function setGroupCard($group_id, $user_id, $card = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'card' => $card
]
], $this->callback);
}
/**
* 设置群名
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_name-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E5%90%8D
* @param $group_id
* @param $group_name
* @return array|bool|null
*/
public function setGroupName($group_id, $group_name) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'group_name' => $group_name
]
], $this->callback);
}
/**
* 退出群组
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_leave-%E9%80%80%E5%87%BA%E7%BE%A4%E7%BB%84
* @param $group_id
* @param bool $is_dismiss
* @return array|bool|null
*/
public function setGroupLeave($group_id, $is_dismiss = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'is_dismiss' => $is_dismiss
]
], $this->callback);
}
/**
* 设置群组专属头衔
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_special_title-%E8%AE%BE%E7%BD%AE%E7%BE%A4%E7%BB%84%E4%B8%93%E5%B1%9E%E5%A4%B4%E8%A1%94
* @param $group_id
* @param $user_id
* @param string $special_title
* @param int $duration
* @return array|bool|null
*/
public function setGroupSpecialTitle($group_id, $user_id, $special_title = "", $duration = -1) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'special_title' => $special_title,
'duration' => $duration
]
], $this->callback);
}
/**
* 处理加好友请求
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_friend_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E5%A5%BD%E5%8F%8B%E8%AF%B7%E6%B1%82
* @param $flag
* @param bool $approve
* @param string $remark
* @return array|bool|null
*/
public function setFriendAddRequest($flag, $approve = true, $remark = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'flag' => $flag,
'approve' => $approve,
'remark' => $remark
]
], $this->callback);
}
/**
* 处理加群请求/邀请
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_group_add_request-%E5%A4%84%E7%90%86%E5%8A%A0%E7%BE%A4%E8%AF%B7%E6%B1%82%E9%82%80%E8%AF%B7
* @param $flag
* @param $sub_type
* @param bool $approve
* @param string $reason
* @return array|bool|null
*/
public function setGroupAddRequest($flag, $sub_type, $approve = true, $reason = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'flag' => $flag,
'sub_type' => $sub_type,
'approve' => $approve,
'reason' => $reason
]
], $this->callback);
}
/**
* 获取登录号信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_login_info-%E8%8E%B7%E5%8F%96%E7%99%BB%E5%BD%95%E5%8F%B7%E4%BF%A1%E6%81%AF
* @return array|bool|null
*/
public function getLoginInfo() {
return $this->processAPI($this->connection, ['action' => $this->getActionName(__FUNCTION__)], $this->callback);
}
/**
* 获取陌生人信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_stranger_info-%E8%8E%B7%E5%8F%96%E9%99%8C%E7%94%9F%E4%BA%BA%E4%BF%A1%E6%81%AF
* @param $user_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getStrangerInfo($user_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'user_id' => $user_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取好友列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_friend_list-%E8%8E%B7%E5%8F%96%E5%A5%BD%E5%8F%8B%E5%88%97%E8%A1%A8
* @return array|bool|null
*/
public function getFriendList() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取群信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E4%BF%A1%E6%81%AF
* @param $group_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getGroupInfo($group_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取群列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E5%88%97%E8%A1%A8
* @return array|bool|null
*/
public function getGroupList() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取群成员信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $user_id
* @param bool $no_cache
* @return array|bool|null
*/
public function getGroupMemberInfo($group_id, $user_id, $no_cache = false) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'user_id' => $user_id,
'no_cache' => $no_cache
]
], $this->callback);
}
/**
* 获取群成员列表
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_member_list-%E8%8E%B7%E5%8F%96%E7%BE%A4%E6%88%90%E5%91%98%E5%88%97%E8%A1%A8
* @param $group_id
* @return array|bool|null
*/
public function getGroupMemberList($group_id) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id
]
], $this->callback);
}
/**
* 获取群荣誉信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_group_honor_info-%E8%8E%B7%E5%8F%96%E7%BE%A4%E8%8D%A3%E8%AA%89%E4%BF%A1%E6%81%AF
* @param $group_id
* @param $type
* @return array|bool|null
*/
public function getGroupHonorInfo($group_id, $type) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'group_id' => $group_id,
'type' => $type
]
], $this->callback);
}
/**
* 获取 CSRF Token
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_csrf_token-%E8%8E%B7%E5%8F%96-csrf-token
* @return array|bool|null
*/
public function getCsrfToken() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取 QQ 相关接口凭证
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_credentials-%E8%8E%B7%E5%8F%96-qq-%E7%9B%B8%E5%85%B3%E6%8E%A5%E5%8F%A3%E5%87%AD%E8%AF%81
* @param string $domain
* @return array|bool|null
*/
public function getCredentials($domain = "") {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'domain' => $domain
]
], $this->callback);
}
/**
* 获取语音
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_record-%E8%8E%B7%E5%8F%96%E8%AF%AD%E9%9F%B3
* @param $file
* @param $out_format
* @return array|bool|null
*/
public function getRecord($file, $out_format) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'file' => $file,
'out_format' => $out_format
]
], $this->callback);
}
/**
* 获取图片
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_image-%E8%8E%B7%E5%8F%96%E5%9B%BE%E7%89%87
* @param $file
* @return array|bool|null
*/
public function getImage($file) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'file' => $file
]
], $this->callback);
}
/**
* 检查是否可以发送图片
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_image-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E5%9B%BE%E7%89%87
* @return array|bool|null
*/
public function canSendImage() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 检查是否可以发送语音
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#can_send_record-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E5%8F%AF%E4%BB%A5%E5%8F%91%E9%80%81%E8%AF%AD%E9%9F%B3
* @return array|bool|null
*/
public function canSendRecord() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取运行状态
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_status-%E8%8E%B7%E5%8F%96%E8%BF%90%E8%A1%8C%E7%8A%B6%E6%80%81
* @return array|bool|null
*/
public function getStatus() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 获取版本信息
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_version_info-%E8%8E%B7%E5%8F%96%E7%89%88%E6%9C%AC%E4%BF%A1%E6%81%AF
* @return array|bool|null
*/
public function getVersionInfo() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
/**
* 重启 OneBot 实现
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#set_restart-%E9%87%8D%E5%90%AF-onebot-%E5%AE%9E%E7%8E%B0
* @param int $delay
* @return array|bool|null
*/
public function setRestart($delay = 0) {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__),
'params' => [
'delay' => $delay
]
], $this->callback);
}
/**
* 清理缓存
* @link https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#clean_cache-%E6%B8%85%E7%90%86%E7%BC%93%E5%AD%98
* @return array|bool|null
*/
public function cleanCache() {
return $this->processAPI($this->connection, [
'action' => $this->getActionName(__FUNCTION__)
], $this->callback);
}
public function callExtendedAPI($action, $params = []) {
return $this->processAPI($this->connection, [
'action' => $action,
'params' => $params
], $this->callback);
}
private function getActionName(string $method) {
$prefix = ($this->prefix == self::API_ASYNC ? '_async' : ($this->prefix == self::API_RATE_LIMITED ? '_rate_limited' : ''));
$func_name = strtolower(preg_replace('/(?<=[a-z])([A-Z])/', '_$1', $method));
return $func_name . $prefix;
}
}

View File

@@ -1,29 +1,38 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation;
use Closure;
abstract class AnnotationBase
{
public $method;
public $method = '';
public $class;
public $class = '';
public function __toString(): string {
$str = __CLASS__ . ": ";
public function __toString()
{
$str = __CLASS__ . ': ';
foreach ($this as $k => $v) {
$str .= "\n\t" . $k . " => ";
if (is_string($v)) $str .= "\"$v\"";
elseif (is_numeric($v)) $str .= $v;
elseif (is_bool($v)) $str .= ($v ? "TRUE" : "FALSE");
elseif (is_array($v)) $str .= json_encode($v, JSON_UNESCAPED_UNICODE);
elseif ($v instanceof Closure) $str .= "@AnonymousFunction";
elseif (is_null($v)) $str .= "NULL";
else $str .= "@Unknown";
$str .= "\n\t" . $k . ' => ';
if (is_string($v)) {
$str .= "\"{$v}\"";
} elseif (is_numeric($v)) {
$str .= $v;
} elseif (is_bool($v)) {
$str .= ($v ? 'TRUE' : 'FALSE');
} elseif (is_array($v)) {
$str .= json_encode($v, JSON_UNESCAPED_UNICODE);
} elseif ($v instanceof Closure) {
$str .= '@AnonymousFunction';
} elseif (is_null($v)) {
$str .= 'NULL';
} else {
$str .= '@Unknown';
}
}
return $str;
}
}
}

View File

@@ -1,18 +1,29 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation;
use Doctrine\Common\Annotations\AnnotationReader;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Console\Console;
use Koriym\Attributes\AttributeReader;
use Koriym\Attributes\DualReader;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ZM\Annotation\Http\{HandleAfter, HandleBefore, HandleException, Middleware, MiddlewareClass, RequestMapping};
use ZM\Annotation\Http\HandleAfter;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\HandleException;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Interfaces\ErgodicAnnotation;
use ZM\Annotation\Interfaces\Level;
use ZM\Annotation\Module\Closed;
use ZM\Http\RouteManager;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Exception\AnnotationException;
use ZM\Utils\Manager\RouteManager;
use ZM\Utils\ZMUtil;
class AnnotationParser
{
@@ -21,23 +32,27 @@ class AnnotationParser
private $start_time;
private $annotation_map = [];
private $middleware_map = [];
private $middlewares = [];
/** @var null|AnnotationReader */
private $reader = null;
private $reader;
private $req_mapping = [];
/**
* AnnotationParser constructor.
*/
public function __construct() {
public function __construct()
{
$this->start_time = microtime(true);
//$this->loadAnnotationClasses();
// $this->loadAnnotationClasses();
$this->req_mapping[0] = [
'id' => 0,
'pid' => -1,
'name' => '/'
'name' => '/',
];
}
@@ -45,19 +60,32 @@ class AnnotationParser
* 注册各个模块类的注解和模块level的排序
* @throws ReflectionException
*/
public function registerMods() {
public function registerMods()
{
foreach ($this->path_list as $path) {
Console::debug("parsing annotation in " . $path[0]);
$all_class = getAllClasses($path[0], $path[1]);
$this->reader = new AnnotationReader();
Console::debug('parsing annotation in ' . $path[0] . ':' . $path[1]);
$all_class = ZMUtil::getClassesPsr4($path[0], $path[1]);
$conf = ZMConfig::get('global', 'runtime')['annotation_reader_ignore'] ?? [];
if (isset($conf['name']) && is_array($conf['name'])) {
foreach ($conf['name'] as $v) {
AnnotationReader::addGlobalIgnoredName($v);
}
}
if (isset($conf['namespace']) && is_array($conf['namespace'])) {
foreach ($conf['namespace'] as $v) {
AnnotationReader::addGlobalIgnoredNamespace($v);
}
}
AnnotationReader::addGlobalIgnoredName('mixin');
$this->reader = new DualReader(new AnnotationReader(), new AttributeReader());
foreach ($all_class as $v) {
Console::debug("正在检索 " . $v);
Console::debug('正在检索 ' . $v);
$reflection_class = new ReflectionClass($v);
$methods = $reflection_class->getMethods(ReflectionMethod::IS_PUBLIC);
$class_annotations = $this->reader->getClassAnnotations($reflection_class);
// 这段为新加的:start
//这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析
// 这里将每个类里面所有的类注解、方法注解通通加到一颗大树上,后期解析
/*
$annotation_map: {
Module\Example\Hello: {
@@ -76,68 +104,85 @@ class AnnotationParser
*/
// 生成主树
$this->annotation_map[$v]["class_annotations"] = $class_annotations;
$this->annotation_map[$v]["methods"] = $methods;
$this->annotation_map[$v]['class_annotations'] = $class_annotations;
$this->annotation_map[$v]['methods'] = $methods;
foreach ($methods as $method) {
$this->annotation_map[$v]["methods_annotations"][$method->getName()] = $this->reader->getMethodAnnotations($method);
$this->annotation_map[$v]['methods_annotations'][$method->getName()] = $this->reader->getMethodAnnotations($method);
}
foreach ($this->annotation_map[$v]["class_annotations"] as $ks => $vs) {
foreach ($this->annotation_map[$v]['class_annotations'] as $vs) {
$vs->class = $v;
//预处理1将适用于每一个函数的注解到类注解重新注解到每个函数下面
// 预处理1将适用于每一个函数的注解到类注解重新注解到每个函数下面
if ($vs instanceof ErgodicAnnotation) {
foreach (($this->annotation_map[$v]["methods"] ?? []) as $method) {
foreach (($this->annotation_map[$v]['methods'] ?? []) as $method) {
$copy = clone $vs;
/** @noinspection PhpUndefinedFieldInspection */
$copy->method = $method->getName();
$this->annotation_map[$v]["methods_annotations"][$method->getName()][] = $copy;
$this->annotation_map[$v]['methods_annotations'][$method->getName()][] = $copy;
}
}
//预处理2处理 class 下面的注解
// 预处理2处理 class 下面的注解
if ($vs instanceof Closed) {
unset($this->annotation_map[$v]);
continue 2;
} elseif ($vs instanceof MiddlewareClass) {
Console::debug("正在注册中间件 " . $reflection_class->getName());
}
if ($vs instanceof MiddlewareClass) {
// 注册中间件本身的类,标记到 middlewares 属性中
Console::debug('正在注册中间件 ' . $reflection_class->getName());
$rs = $this->registerMiddleware($vs, $reflection_class);
$this->middlewares[$rs["name"]] = $rs;
$this->middlewares[$rs['name']] = $rs;
}
}
//预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach (($this->annotation_map[$v]["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
$inserted = [];
// 预处理3处理每个函数上面的特殊注解就是需要操作一些东西的
foreach (($this->annotation_map[$v]['methods_annotations'] ?? []) as $method_name => $methods_annotations) {
foreach ($methods_annotations as $method_anno) {
/** @var AnnotationBase $method_anno */
/* @var AnnotationBase $method_anno */
$method_anno->class = $v;
$method_anno->method = $method_name;
if (!($method_anno instanceof Middleware) && ($middlewares = ZMConfig::get('global', 'runtime')['global_middleware_binding'][get_class($method_anno)] ?? []) !== []) {
if (!isset($inserted[$v][$method_name])) {
// 在这里在其他中间件前插入插入全局的中间件
foreach ($middlewares as $middleware) {
$mid_class = new Middleware($middleware);
$mid_class->class = $v;
$mid_class->method = $method_name;
$this->middleware_map[$v][$method_name][] = $mid_class;
}
$inserted[$v][$method_name] = true;
}
}
if ($method_anno instanceof RequestMapping) {
RouteManager::importRouteByAnnotation($method_anno, $method_name, $v, $methods_annotations);
} elseif ($method_anno instanceof Middleware) {
$this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno->middleware;
$this->middleware_map[$method_anno->class][$method_anno->method][] = $method_anno;
}
}
}
}
}
Console::debug("解析注解完毕!");
Console::debug('解析注解完毕!');
}
public function generateAnnotationEvents(): array {
public function generateAnnotationEvents(): array
{
$o = [];
foreach ($this->annotation_map as $module => $obj) {
foreach (($obj["class_annotations"] ?? []) as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) continue;
else $o[get_class($class_annotation)][] = $class_annotation;
foreach ($this->annotation_map as $obj) {
// 这里的ErgodicAnnotation是为了解决类上的注解可穿透到方法上的问题
foreach (($obj['class_annotations'] ?? []) as $class_annotation) {
if ($class_annotation instanceof ErgodicAnnotation) {
continue;
}
$o[get_class($class_annotation)][] = $class_annotation;
}
foreach (($obj["methods_annotations"] ?? []) as $method_name => $methods_annotations) {
foreach (($obj['methods_annotations'] ?? []) as $methods_annotations) {
foreach ($methods_annotations as $annotation) {
$o[get_class($annotation)][] = $annotation;
}
}
}
foreach ($o as $k => $v) {
$this->sortByLevel($o, $k);
@@ -145,49 +190,36 @@ class AnnotationParser
return $o;
}
/**
* @return array
*/
public function getMiddlewares(): array { return $this->middlewares; }
public function getMiddlewares(): array
{
return $this->middlewares;
}
/**
* @return array
*/
public function getMiddlewareMap(): array { return $this->middleware_map; }
public function getMiddlewareMap(): array
{
return $this->middleware_map;
}
/**
* @return array
*/
public function getReqMapping(): array { return $this->req_mapping; }
public function getReqMapping(): array
{
return $this->req_mapping;
}
/**
* @param $path
* @param $indoor_name
*/
public function addRegisterPath($path, $indoor_name) { $this->path_list[] = [$path, $indoor_name]; }
//private function below
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class): array {
$result = [
"class" => "\\" . $reflection_class->getName(),
"name" => $vs->name
];
foreach ($reflection_class->getMethods() as $vss) {
$method_annotations = $this->reader->getMethodAnnotations($vss);
foreach ($method_annotations as $vsss) {
if ($vsss instanceof HandleBefore) $result["before"] = $vss->getName();
if ($vsss instanceof HandleAfter) $result["after"] = $vss->getName();
if ($vsss instanceof HandleException) {
$result["exceptions"][$vsss->class_name] = $vss->getName();
}
}
}
return $result;
public function addRegisterPath($path, $indoor_name)
{
$this->path_list[] = [$path, $indoor_name];
}
public function sortByLevel(&$events, string $class_name, $prefix = "") {
/**
* @param $events
* @internal 用于 level 排序
*/
public function sortByLevel(&$events, string $class_name, string $prefix = '')
{
if (is_a($class_name, Level::class, true)) {
$class_name .= $prefix;
usort($events[$class_name], function ($a, $b) {
@@ -197,4 +229,54 @@ class AnnotationParser
});
}
}
/**
* @throws AnnotationException
*/
public function verifyMiddlewares()
{
if ((ZMConfig::get('global', 'runtime')['middleware_error_policy'] ?? 1) === 2) {
// 我承认套三层foreach很不优雅但是这个会很快的。
foreach ($this->middleware_map as $v) {
foreach ($v as $vs) {
foreach ($vs as $mid) {
if (!isset($this->middlewares[$mid->middleware])) {
throw new AnnotationException("Annotation parse error: Unknown MiddlewareClass named \"{$mid->middleware}\"!");
}
}
}
}
}
}
public function getRunTime()
{
return microtime(true) - $this->start_time;
}
// private function below
private function registerMiddleware(MiddlewareClass $vs, ReflectionClass $reflection_class): array
{
$result = [
'class' => '\\' . $reflection_class->getName(),
'name' => $vs->name,
];
foreach ($reflection_class->getMethods() as $vss) {
$method_annotations = $this->reader->getMethodAnnotations($vss);
foreach ($method_annotations as $vsss) {
if ($vsss instanceof HandleBefore) {
$result['before'] = $vss->getName();
}
if ($vsss instanceof HandleAfter) {
$result['after'] = $vss->getName();
}
if ($vsss instanceof HandleException) {
$result['exceptions'][$vsss->class_name] = $vss->getName();
}
}
}
return $result;
}
}

View File

@@ -1,19 +1,22 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class CQAPIResponse
* @package ZM\Annotation\CQ
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class CQAPIResponse extends AnnotationBase
{
/**
@@ -21,4 +24,9 @@ class CQAPIResponse extends AnnotationBase
* @Required()
*/
public $retcode;
public function __construct($retcode)
{
$this->retcode = $retcode;
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
@@ -10,9 +13,10 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQAfter
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class CQAfter extends AnnotationBase implements Level
{
/**
@@ -23,17 +27,25 @@ class CQAfter extends AnnotationBase implements Level
public $level = 20;
public function __construct($cq_event, $level = 20)
{
$this->cq_event = $cq_event;
$this->level = $level;
}
/**
* @return mixed
*/
public function getLevel(): int {
public function getLevel()
{
return $this->level;
}
/**
* @param mixed $level
*/
public function setLevel($level) {
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@@ -1,9 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
@@ -12,9 +14,10 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQBefore
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class CQBefore extends AnnotationBase implements Level
{
/**
@@ -25,18 +28,25 @@ class CQBefore extends AnnotationBase implements Level
public $level = 20;
public function __construct($cq_event, $level = 20)
{
$this->cq_event = $cq_event;
$this->level = $level;
}
/**
* @return mixed
*/
public function getLevel(): int {
public function getLevel(): int
{
return $this->level;
}
/**
* @param mixed $level
*/
public function setLevel($level) {
public function setLevel($level)
{
$this->level = $level;
}
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
@@ -10,44 +13,74 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQCommand
* @Annotation
* @NamedArgumentConstructor
* @Target("ALL")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CQCommand extends AnnotationBase implements Level
{
/** @var string */
public $match = "";
public $match = '';
/** @var string */
public $pattern = "";
public $pattern = '';
/** @var string */
public $regex = "";
public $regex = '';
/** @var string */
public $start_with = "";
public $start_with = '';
/** @var string */
public $end_with = "";
public $end_with = '';
/** @var string */
public $keyword = "";
public $keyword = '';
/** @var string[] */
public $alias = [];
/** @var string */
public $message_type = "";
public $message_type = '';
/** @var int */
public $user_id = 0;
/** @var int */
public $group_id = 0;
/** @var int */
public $discuss_id = 0;
/** @var int */
public $level = 20;
/**
* @return int
*/
public function getLevel(): int { return $this->level; }
public function __construct($match = '', $pattern = '', $regex = '', $start_with = '', $end_with = '', $keyword = '', $alias = [], $message_type = '', $user_id = 0, $group_id = 0, $discuss_id = 0, $level = 20)
{
$this->match = $match;
$this->pattern = $pattern;
$this->regex = $regex;
$this->start_with = $start_with;
$this->end_with = $end_with;
$this->keyword = $keyword;
$this->alias = $alias;
$this->message_type = $message_type;
$this->user_id = $user_id;
$this->group_id = $group_id;
$this->discuss_id = $discuss_id;
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) { $this->level = $level; }
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
@@ -10,31 +13,53 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQMessage
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CQMessage extends AnnotationBase implements Level
{
/**
* @var string
*/
public $message_type = "";
public $message_type = '';
/** @var int */
public $user_id = 0;
/** @var int */
public $group_id = 0;
/** @var int */
public $discuss_id = 0;
/** @var string */
public $message = "";
public $message = '';
/** @var string */
public $raw_message = "";
public $raw_message = '';
/** @var int */
public $level = 20;
public function getLevel(): int { return $this->level; }
public function setLevel(int $level) {
public function __construct($message_type = '', $user_id = 0, $group_id = 0, $discuss_id = 0, $message = '', $raw_message = '', $level = 20)
{
$this->message_type = $message_type;
$this->user_id = $user_id;
$this->group_id = $group_id;
$this->discuss_id = $discuss_id;
$this->message = $message;
$this->raw_message = $raw_message;
$this->level = $level;
}
}
public function getLevel(): int
{
return $this->level;
}
public function setLevel($level)
{
$this->level = $level;
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
@@ -11,28 +14,40 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQMetaEvent
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CQMetaEvent extends AnnotationBase implements Level
{
/**
* @var string
* @Required()
*/
public $meta_event_type = '';
public $meta_event_type;
/** @var int */
public $level;
public $level = 20;
public function __construct($meta_event_type, $level = 20)
{
$this->meta_event_type = $meta_event_type;
$this->level = $level;
}
/**
* @return mixed
*/
public function getLevel(): int { return $this->level; }
public function getLevel(): int
{
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
public function setLevel($level)
{
$this->level = $level;
}
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
@@ -10,33 +13,46 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQNotice
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CQNotice extends AnnotationBase implements Level
{
/** @var string */
public $notice_type = "";
public $notice_type = '';
/** @var string */
public $sub_type = "";
public $sub_type = '';
/** @var int */
public $group_id = 0;
/** @var int */
public $operator_id = 0;
/** @var int */
public $level = 20;
/**
* @return int
*/
public function getLevel(): int {
public function __construct($notice_type = '', $sub_type = '', $group_id = 0, $operator_id = 0, $level = 20)
{
$this->notice_type = $notice_type;
$this->sub_type = $sub_type;
$this->group_id = $group_id;
$this->operator_id = $operator_id;
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
public function setLevel($level)
{
$this->level = $level;
}
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\CQ;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Level;
@@ -10,33 +13,46 @@ use ZM\Annotation\Interfaces\Level;
/**
* Class CQRequest
* @Annotation
* @NamedArgumentConstructor()
* @Target("ALL")
* @package ZM\Annotation\CQ
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class CQRequest extends AnnotationBase implements Level
{
/** @var string */
public $request_type = "";
public $request_type = '';
/** @var string */
public $sub_type = "";
public $sub_type = '';
/** @var int */
public $user_id = 0;
/** @var string */
public $comment = "";
public $comment = '';
/** @var int */
public $level = 20;
/**
* @return int
*/
public function getLevel(): int {
public function __construct($request_type = '', $sub_type = '', $user_id = 0, $comment = '', $level = 20)
{
$this->request_type = $request_type;
$this->sub_type = $sub_type;
$this->user_id = $user_id;
$this->comment = $comment;
$this->level = $level;
}
public function getLevel(): int
{
return $this->level;
}
/**
* @param int $level
*/
public function setLevel(int $level) {
public function setLevel($level)
{
$this->level = $level;
}
}
}

View File

@@ -1,18 +1,22 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\Command;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class TerminalCommand
* @package ZM\Annotation\Command
* @Annotation
* @NamedArgumentConstructor
* @Target("METHOD")
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class TerminalCommand extends AnnotationBase
{
/**
@@ -26,5 +30,12 @@ class TerminalCommand extends AnnotationBase
/**
* @var string
*/
public $description = "";
}
public $description = '';
public function __construct($command, $alias = '', $description = '')
{
$this->command = $command;
$this->alias = $alias;
$this->description = $description;
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\Http;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Required;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
@@ -11,9 +14,10 @@ use ZM\Annotation\Interfaces\ErgodicAnnotation;
/**
* Class Controller
* @Annotation
* @NamedArgumentConstructor()
* @Target("CLASS")
* @package ZM\Annotation\Http
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)]
class Controller extends AnnotationBase implements ErgodicAnnotation
{
/**
@@ -21,4 +25,9 @@ class Controller extends AnnotationBase implements ErgodicAnnotation
* @Required()
*/
public $prefix = '';
public function __construct(string $prefix)
{
$this->prefix = $prefix;
}
}

View File

@@ -1,18 +1,21 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\Http;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class HandleAfter
* @package ZM\Annotation\Http
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class HandleAfter extends AnnotationBase
{
}

View File

@@ -1,18 +1,21 @@
<?php
declare(strict_types=1);
namespace ZM\Annotation\Http;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class HandleBefore
* @package ZM\Annotation\Http
* @Annotation
* @NamedArgumentConstructor()
* @Target("METHOD")
*/
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_METHOD)]
class HandleBefore extends AnnotationBase
{
}

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