Compare commits
227 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6e135a642 | ||
|
|
d3f4ade215 | ||
|
|
56d2a52706 | ||
|
|
b40dc1e05e | ||
|
|
bed3337866 | ||
|
|
28dc73d060 | ||
|
|
5ea1226ad0 | ||
|
|
e43f2a4158 | ||
|
|
d73e771ef3 | ||
|
|
c077e9418c | ||
|
|
2ce888c84c | ||
|
|
b4d175ff98 | ||
|
|
35e18d0481 | ||
|
|
f6cccb00e3 | ||
|
|
2a82c82039 | ||
|
|
a2e8a1b582 | ||
|
|
dbd78d4b86 | ||
|
|
8105892b6e | ||
|
|
bc7d5871e4 | ||
|
|
680e6a8c5f | ||
|
|
c364084cb2 | ||
|
|
63f8042746 | ||
|
|
48e6b2cea0 | ||
|
|
228309c571 | ||
|
|
ca3a3df3ba | ||
|
|
c7df37b17c | ||
|
|
41f03fbba4 | ||
|
|
b3089c1bba | ||
|
|
c5a6f1fea4 | ||
|
|
ba2777137b | ||
|
|
6d90be164a | ||
|
|
4da6f5859a | ||
|
|
15d4ea710a | ||
|
|
f0541c1f32 | ||
|
|
2b8cab1824 | ||
|
|
61c7972915 | ||
|
|
44a0eec74c | ||
|
|
e57753e44b | ||
|
|
74e91a2950 | ||
|
|
7ce3ef41df | ||
|
|
444a77933a | ||
|
|
78f78c607d | ||
|
|
69155002dc | ||
|
|
475d14fab7 | ||
|
|
f4d7e63358 | ||
|
|
f222d2b45b | ||
|
|
d0155fe1da | ||
|
|
4737d0b507 | ||
|
|
09bd0197bb | ||
|
|
f0f120bd32 | ||
|
|
c897da29c6 | ||
|
|
e347e254e8 | ||
|
|
12363aebf0 | ||
|
|
ff0b925313 | ||
|
|
a6b4bd9b80 | ||
|
|
485fa5476c | ||
|
|
689076d97c | ||
|
|
cca6102e91 | ||
|
|
095855162b | ||
|
|
326f934013 | ||
|
|
35b0c258fe | ||
|
|
6650846b15 | ||
|
|
a33d320f4c | ||
|
|
0bcfea6aa4 | ||
|
|
db6e63e91c | ||
|
|
a7f84fb53a | ||
|
|
ce7f2b1765 | ||
|
|
abbfb59eff | ||
|
|
b57fef16f9 | ||
|
|
1706afbcd0 | ||
|
|
d01bd69aa5 | ||
|
|
e6b9ae3ee1 | ||
|
|
73b6b8045d | ||
|
|
3c87abc6e8 | ||
|
|
e95925c129 | ||
|
|
82c44d6c40 | ||
|
|
e0a268e05e | ||
|
|
487892e1d9 | ||
|
|
7ab4e88359 | ||
|
|
4702b6ee75 | ||
|
|
20cd3aa66d | ||
|
|
391114bdef | ||
|
|
ffe1052ecc | ||
|
|
b4159152a7 | ||
|
|
8fc6e4b0f7 | ||
|
|
c8938b7a4b | ||
|
|
34db1626a5 | ||
|
|
7f0c97c5b9 | ||
|
|
74050c46e7 | ||
| 3ed1cb665a | |||
|
|
a6f33ba69d | ||
|
|
176b690417 | ||
|
|
e22b1b90ec | ||
|
|
a3c560790c | ||
| 570e2108dd | |||
|
|
59c0d95e5d | ||
|
|
9ceaecdc02 | ||
|
|
19d50898ef | ||
|
|
5b62ca62ae | ||
| fb528d30ce | |||
| 5f2d5ed334 | |||
| c20e459900 | |||
| 09220825cf | |||
|
|
3d4db23d27 | ||
|
|
4496b67dcc | ||
|
|
2a13298384 | ||
|
|
d6ec404d76 | ||
|
|
3235fd4dc1 | ||
|
|
293740fee2 | ||
|
|
d3c420ec84 | ||
|
|
85ef09d43c | ||
|
|
e4561d69c4 | ||
|
|
6b4d206099 | ||
|
|
50843edf6a | ||
|
|
3a1686f8da | ||
|
|
66dd91bb97 | ||
|
|
e020e5d593 | ||
|
|
3d62663281 | ||
|
|
8d9485c02e | ||
|
|
9fb45dd683 | ||
|
|
8a4924dba9 | ||
|
|
beaf7be606 | ||
|
|
7dbd21bdf4 | ||
|
|
9ce3056203 | ||
|
|
880b4e847c | ||
|
|
71e83d5bc8 | ||
|
|
432fd92cca | ||
|
|
907a9cea25 | ||
| 07391810ff | |||
| 5063421364 | |||
| 2b4d308783 | |||
| e2f49968b3 | |||
| 229778ebf9 | |||
|
|
d2c0972c93 | ||
|
|
d300b6e518 | ||
|
|
56cb7b2223 | ||
|
|
2fc42d5d60 | ||
|
|
999e90f709 | ||
|
|
45c6cd7d2a | ||
|
|
5aa0858021 | ||
|
|
e981da3932 | ||
|
|
2685be7306 | ||
|
|
a13c4628f5 | ||
|
|
b36417454c | ||
|
|
a6f5952dee | ||
|
|
d67dfe46f6 | ||
|
|
e57cc43500 | ||
|
|
481063285b | ||
|
|
d805523dbd | ||
|
|
58267f66fc | ||
|
|
48215f2e5e | ||
|
|
7e0fc1528a | ||
|
|
c185d20a93 | ||
|
|
7ec847e576 | ||
|
|
4ee16d4fc6 | ||
|
|
59d614a24e | ||
|
|
40037b3f4b | ||
|
|
f91c8b6205 | ||
|
|
8f73a99ff7 | ||
|
|
e0f07cb396 | ||
|
|
2950ab7472 | ||
|
|
0ab4053dfb | ||
|
|
745aa0f268 | ||
|
|
12bb93c2f0 | ||
| 95ca175901 | |||
| 71585ed29d | |||
|
|
231a377718 | ||
|
|
a80ee902a9 | ||
|
|
c2d3b5f92a | ||
|
|
64365af124 | ||
|
|
60619dbffc | ||
|
|
77e77e9cc3 | ||
|
|
d72b41a902 | ||
|
|
dfddaaea94 | ||
|
|
6b872c6f74 | ||
|
|
202c8aee77 | ||
|
|
ba397a1744 | ||
|
|
d699a152d5 | ||
|
|
beef44ea50 | ||
|
|
b991a2da7b | ||
|
|
bbe4addd83 | ||
|
|
600829645d | ||
|
|
68280cfe7e | ||
|
|
c5523aa95d | ||
|
|
93a68a5582 | ||
|
|
6155236d3c | ||
| 28f7f20728 | |||
| 235256d679 | |||
| 626d569858 | |||
| 0492179bdd | |||
| 1dfd1de5c1 | |||
| d15d01c32b | |||
|
|
c9f4278d9b | ||
|
|
6aa0540c9e | ||
|
|
9689dc9db1 | ||
|
|
c20e3324d4 | ||
| 303f44cd2b | |||
| 66b73973b4 | |||
|
|
0ff4e52ed3 | ||
| b6d1f724e9 | |||
| e77b9d4970 | |||
|
|
456b102c15 | ||
|
|
cc57997abc | ||
|
|
19e61c7cc3 | ||
|
|
f908513dca | ||
|
|
7dc39e6ada | ||
|
|
b0be53554d | ||
|
|
b98048bd39 | ||
|
|
fffd3fdc95 | ||
|
|
dea9ed2ccd | ||
|
|
de5744c9e4 | ||
|
|
a23f3d8f16 | ||
|
|
0c24bfdedd | ||
|
|
c0b95c6840 | ||
|
|
e977b09e20 | ||
|
|
4ff75cf199 | ||
|
|
24e70c70ce | ||
|
|
275a7bf00b | ||
|
|
455fc79818 | ||
|
|
8740c3c255 | ||
|
|
98bfca5bb9 | ||
|
|
fc8d01ad5f | ||
|
|
d9b8df1725 | ||
|
|
9b7802ac04 | ||
|
|
6e1f3e0406 | ||
|
|
a2d4bab062 | ||
|
|
f1cefad910 |
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
if [ ! -d "/app/zhamao-framework/bin" ]; then
|
||||
cp -r /app/zhamao-framework-bak/* /app/zhamao-framework/
|
||||
fi
|
||||
php /app/zhamao-framework/bin/start
|
||||
38
.github/ISSUE_TEMPLATE/1_Bug_report.yaml
vendored
Normal 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: 其他可能有帮助的信息,如日志、截图等
|
||||
19
.github/ISSUE_TEMPLATE/2_Feature_request.yaml
vendored
Normal 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: |
|
||||
一个简单的例子,展示该功能将如何被使用(包括代码、配置文件等)
|
||||
如果这是针对已有功能的改进,请展示改进前后使用方式(或效能)的对比
|
||||
65
.github/workflows/integration-test.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
- review_requested
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
|
||||
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"
|
||||
32
.github/workflows/vuepress-deploy.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: VuePress 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: jenkey2011/vuepress-deploy@master
|
||||
env:
|
||||
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_SCRIPT: yarn && yarn docs:build
|
||||
BUILD_DIR: docs/.vuepress/dist/
|
||||
- name: Copy deployment to current folder
|
||||
run: |
|
||||
cp -r "${GITHUB_WORKSPACE}/docs/.vuepress/dist" "./deploy"
|
||||
- name: Deploy to Zhamao Server
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.ZHAMAO_XIN_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "deploy/"
|
||||
REMOTE_HOST: ${{ secrets.ZHAMAO_XIN_HOST }}
|
||||
REMOTE_USER: ${{ secrets.ZHAMAO_XIN_USER }}
|
||||
TARGET: ${{ secrets.ZHAMAO_XIN_TARGET }}
|
||||
25
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
/src/test/
|
||||
/src/webconsole/config/
|
||||
/vendor/
|
||||
zm.json
|
||||
/zm_data/
|
||||
composer.lock
|
||||
/resources/server.phar
|
||||
@@ -10,4 +9,26 @@ composer.lock
|
||||
/bin/.phpunit.result.cache
|
||||
/resources/zhamao.service
|
||||
.phpunit.result.cache
|
||||
.daemon_pid
|
||||
.daemon_pid
|
||||
/runtime/
|
||||
/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
|
||||
|
||||
# Vuepress 相关文件
|
||||
/node_modules/
|
||||
/docs/.vuepress/dist/
|
||||
package-lock.json
|
||||
75
.php-cs-fixer.php
Normal 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);
|
||||
17
.run/Reload Zhamao Server.run.xml
Normal 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 && 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>
|
||||
17
.run/Run Zhamao Server.run.xml
Normal 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
@@ -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>
|
||||
17
.run/Stop Zhamao Server.run.xml
Normal 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 && 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>
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM zmbot/swoole:latest
|
||||
|
||||
# TODO: auto-setup entrypoint
|
||||
72
README.md
@@ -7,21 +7,23 @@
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
|
||||
[](https://packagist.org/packages/zhamao/framework)
|
||||
[](https://github.com/howmanybots/onebot)
|
||||

|
||||
|
||||
[](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
|
||||
[](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 参与开发!如果对框架本身的核心设计有更好的想法,可与作者成立开发组(目前仅 2 人),参与 OneBot V12 生态和框架本身的开发。**
|
||||
|
||||
2.0 版本如果有问题请第一时间加群反馈!
|
||||
**相关正在进行的版本任务见 Projects 一栏!**
|
||||
|
||||
## 简介
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
|
||||
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务,聊天机器人(OneBot 兼容的 QQ 机器人对接),包含 Websocket、HTTP
|
||||
等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
|
||||
|
||||
框架主要用途为 HTTP 服务器,机器人搭建框架。尤其对于 QQ 机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
|
||||
|
||||
@@ -40,33 +42,54 @@ public function index() {
|
||||
}
|
||||
```
|
||||
|
||||
> 从 2.7.0 版本开始,框架已支持同时使用 Annotation 和原生 Attribute 注解,供开发者根据需要自由选用。
|
||||
|
||||
## 开始
|
||||
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。
|
||||
|
||||
## 文档(v2 版本)
|
||||
查看文档(国内自建):<https://docs-v2.zhamao.xin/>
|
||||
如果你是初学者,可以直接使用以下脚本部署 PHP 环境和安装框架的脚手架:
|
||||
|
||||
备用链接(国外托管):<https://docs-v2.zhamao.me/>
|
||||
```bash
|
||||
# 新建一个自己喜欢名字的文件夹,运行一键安装脚本 (仅限 x86-64(AMD64) 和 AArch64(ARM64) 平台)
|
||||
mkdir zhamao-app/
|
||||
cd zhamao-app/
|
||||
# 默认安装的 PHP 版本为 7.4,如需使用其他版本,请设置环境变量 ZM_DOWN_PHP_VERSION 为对应的 PHP 版本,例如:
|
||||
# export ZM_DOWN_PHP_VERSION=8.1
|
||||
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
|
||||
|
||||
自行构建文档:`mkdocs build -d distribute`
|
||||
# 启动
|
||||
./zhamao server:start
|
||||
```
|
||||
|
||||
关于其他安装方式,请参阅[文档](https://framework.zhamao.xin/guide/installation.html) 。
|
||||
|
||||
## 文档
|
||||
|
||||
查看文档(国内自建):<https://framework.zhamao.xin/>
|
||||
|
||||
备用链接(国外托管):<https://framework.zhamao.me/>
|
||||
|
||||
## 特点
|
||||
- 原生为多账号设计,支持多个机器人负载均衡
|
||||
|
||||
- 原生支持多个机器人客户端同时连接
|
||||
- 使用 Swoole 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
|
||||
- 灵活的注解事件绑定机制
|
||||
- 灵活的注解事件绑定机制,可同时使用 Annotation 和原生 Attribute 注解
|
||||
- 易用的上下文,模块内随处可用
|
||||
- 采用模块化编写,可自由搭配其他 composer 组件
|
||||
- 采用模块化编写,可自由搭配其他 Composer 组件,也可单文件面向过程编写
|
||||
- 支持模块打包、热加载,分享模块更方便
|
||||
- 常驻内存,全局缓存变量随处使用,提供多种缓存方案
|
||||
- 自带 MySQL、Redis 等数据库连接池等数据库连接方案
|
||||
- 本身为 HTTP 服务器、WebSocket 服务器,可以构建属于自己的 HTTP API 接口
|
||||
- 静态文件服务器,可将前端合并到一起
|
||||
- 自带 PHP + Swoole 环境,无需手动编译安装,by [crazywhalecc/static-php-cli](https://github.com/crazywhalecc/static-php-cli)
|
||||
|
||||
## 从 v1 升级
|
||||
炸毛框架 v2 相对 v1 版本改动了不少内容,其中包括框架底层机制、注解事件分发、调试、命名空间等变化,详情可查看上方文档。
|
||||
## 下载源码
|
||||
|
||||
如果旧版框架使用过程中无问题且对新功能暂无需求,可以继续使用 v1 版本,后续也将维护安全类更新和修复致命 bug。
|
||||
框架源码可直接克隆本仓库进行编辑,如果你在国内,访问 GitHub 和克隆仓库比较慢,可以将 `github.com` 替换为 `fgit.zhamao.me` 进行加速。
|
||||
|
||||
例如:`git clone https://fgit.zhamao.me/zhamao-robot/zhamao-framework.git --depth 1`。
|
||||
|
||||
## 贡献和捐赠
|
||||
|
||||
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
|
||||
|
||||
目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
|
||||
@@ -75,17 +98,20 @@ public function index() {
|
||||
|
||||
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
|
||||
|
||||
如果您不想直接参与框架的开发,也可以分享你编写的模块,帮助完善框架生态。
|
||||
|
||||
### 支付宝
|
||||
|
||||

|
||||
|
||||
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ,我们会作为您捐助了本项目!
|
||||
|
||||
## 关于
|
||||
|
||||
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
|
||||
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**三年**,并且持续迭代。
|
||||
作者的炸毛机器人已从2018年初起稳定运行了**四年半**,并且持续迭代。
|
||||
|
||||
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ([627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))或提交 Issue 进行疑难解答。
|
||||
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ([627577391](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes))
|
||||
或提交 [Issue](https://github.com/zhamao-robot/zhamao-framework/issues/new/choose) 进行疑难解答。
|
||||
|
||||
本项目在更新内容时,请及时关注 GitHub 动态,更新前请将自己的模块代码做好备份。
|
||||
|
||||
@@ -95,4 +121,10 @@ public function index() {
|
||||
|
||||
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 GitHub 等网站。
|
||||
|
||||

|
||||
感谢 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 中为项目开发规范化提出的一些建议。
|
||||
|
||||
<!--  -->
|
||||
|
||||
14
SECURITY.md
@@ -1,14 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 2.0 | :white_check_mark: |
|
||||
| 1.6.x | :white_check_mark: |
|
||||
| 1.1.x | :x: |
|
||||
| 1.0.x | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a bug which is safety related, you should post a new issue named **Security Issue**, and I will check it as soon as possible.
|
||||
20
bin/phpunit-swoole
Executable file → Normal file
@@ -6,13 +6,15 @@
|
||||
* 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 ([
|
||||
@@ -52,14 +54,16 @@ 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";
|
||||
|
||||
44
bin/start
@@ -1,14 +1,34 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
#!/bin/sh
|
||||
|
||||
if (!is_dir(__DIR__ . '/../vendor')) {
|
||||
define("LOAD_MODE", 1); //composer项目模式
|
||||
define("LOAD_MODE_COMPOSER_PATH", getcwd());
|
||||
/** @noinspection PhpIncludeInspection */
|
||||
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
|
||||
} else {
|
||||
define("LOAD_MODE", 0); //源码模式
|
||||
require_once __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
|
||||
|
||||
31
bin/systemd
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env php
|
||||
<?php /** @since 1.2 */
|
||||
switch ($argv[1] ?? '') {
|
||||
case '--generate':
|
||||
case '':
|
||||
generate($argv);
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
default:
|
||||
echo "\nUsage: " . $argv[0] . " [OPTION]\n";
|
||||
echo "\nzhamao-framework systemd generator.";
|
||||
echo "\n\n -h, --help\t\tShow this help menu";
|
||||
echo "\n --generate\tGenerate a systemd service file\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
function generate($argv) {
|
||||
$s = "[Unit]\nDescription=zhamao-framework Daemon\nAfter=rc-local.service\n\n[Service]\nType=simple";
|
||||
$s .= "\nUser=" . exec("whoami");
|
||||
$s .= "\nGroup=" . exec("groups | awk '{print $1}'");
|
||||
$s .= "\nWorkingDirectory=" . getcwd();
|
||||
if ($argv[0] == "systemd" && !file_exists(getcwd() . '/systemd'))
|
||||
$s .= "\nExecStart=" . getcwd() . "/vendor/bin/start server --disable-console-input";
|
||||
else
|
||||
$s .= "\nExecStart=" . getcwd() . "/bin/start server --disable-console-input";
|
||||
$s .= "\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n";
|
||||
@mkdir(getcwd() . "/resources/");
|
||||
file_put_contents(getcwd() . "/resources/zhamao.service", $s);
|
||||
echo "File successfully generated. Path: " . getcwd() . "/resources/zhamao.service\n";
|
||||
}
|
||||
118
bin/watcher
Executable 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
|
||||
@@ -1,47 +1,62 @@
|
||||
{
|
||||
"name": "zhamao/framework",
|
||||
"description": "High performance QQ robot and web server development framework",
|
||||
"description": "High performance chat robot and web server development framework",
|
||||
"minimum-stability": "stable",
|
||||
"license": "Apache-2.0",
|
||||
"version": "2.2.7",
|
||||
"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": [
|
||||
{
|
||||
"name": "whale",
|
||||
"email": "crazysnowcc@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "swift",
|
||||
"email": "hugo_swift@yahoo.com"
|
||||
"name": "jerry",
|
||||
"email": "admin@zhamao.me"
|
||||
}
|
||||
],
|
||||
"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/connection-manager": "*@dev",
|
||||
"zhamao/console": "^1.0",
|
||||
"doctrine/dbal": "^2.13.1",
|
||||
"jelix/version": "^2.0",
|
||||
"koriym/attributes": "^1.0",
|
||||
"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": "*@dev"
|
||||
"zhamao/connection-manager": "^1.0",
|
||||
"zhamao/console": "^1.0",
|
||||
"zhamao/request": "^1.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ctype": "*",
|
||||
"ext-mbstring": "*"
|
||||
"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": {
|
||||
"psr-4": {
|
||||
@@ -51,8 +66,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",
|
||||
"phpunit/phpunit": "^9.5"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,152 @@
|
||||
<?php
|
||||
/** @noinspection PhpFullyQualifiedNameUsageInspection */
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
global $config;
|
||||
|
||||
/** bind host */
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
|
||||
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,
|
||||
//'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即将废弃) */
|
||||
$config['onebot'] = [
|
||||
'status' => true,
|
||||
'single_bot_mode' => false,
|
||||
'message_level' => 99,
|
||||
'message_convert_string' => true,
|
||||
'message_command_policy' => 'interrupt',
|
||||
];
|
||||
|
||||
/** 服务器启用的外部第三方和内部插件 */
|
||||
$config['modules'] = [
|
||||
'onebot' => [
|
||||
'status' => true,
|
||||
'single_bot_mode' => false
|
||||
], // QQ机器人事件解析器,如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭
|
||||
/* 一个远程简易终端,使用nc直接连接即可,但是不建议开放host为0.0.0.0(远程连接) */
|
||||
$config['remote_terminal'] = [
|
||||
'status' => false,
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 20002,
|
||||
'token' => '',
|
||||
];
|
||||
|
||||
/* 模块(插件)加载器的相关设置 */
|
||||
$config['module_loader'] = [
|
||||
'enable_hotload' => false,
|
||||
'load_path' => $config['zm_data'] . 'modules',
|
||||
];
|
||||
|
||||
return $config;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
______
|
||||
|__ / |__ __ _ _ __ ___ __ _ ___
|
||||
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
|
||||
/ /_| | | | (_| | | | | | | (_| | (_) |
|
||||
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
|
||||
|
||||
|
||||
141
docs/.vuepress/components/ChatBox.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="doc-chat-container">
|
||||
<div class="doc-chat-content">
|
||||
<div v-for="i in chat" v-bind="i">
|
||||
<div class="doc-chat-row" v-if="i.type === 0">
|
||||
<div class="doc-chat-box">{{ i.content }}</div>
|
||||
<img class="doc-chat-avatar" src="http://api.btstu.cn/sjtx/api.php" alt=""/>
|
||||
</div>
|
||||
<div class="doc-chat-row doc-chat-row-robot" v-else-if="i.type === 1">
|
||||
<img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>
|
||||
<div class="doc-chat-box doc-chat-box-robot">
|
||||
<span v-for="(p,index) in i.content.split('\n')">{{p}}<br v-if="index !== i.content.length - 1"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-chat-row doc-chat-banner" v-else-if="i.type === 2">
|
||||
{{ i.content }}
|
||||
</div>
|
||||
<div class="doc-chat-row doc-chat-row-robot" v-else-if="i.type === 3">
|
||||
<img class="doc-chat-avatar" src="https://docs-v1.zhamao.xin/logo.png" alt=""/>
|
||||
<div class="doc-chat-box doc-chat-box-robot">
|
||||
<img :src="i.content" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
type:
|
||||
0: 我方发送消息
|
||||
1: 机器人回复消息
|
||||
2: 显示一个横幅系统消息
|
||||
3: 机器人回复一个图片
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ChatBox",
|
||||
props: ['myChats'],
|
||||
data() {
|
||||
return {
|
||||
chat: this.myChats,
|
||||
multiline: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.doc-chat-content {
|
||||
padding: 12px;
|
||||
}
|
||||
.doc-chat-container {
|
||||
border-radius: 6px;
|
||||
max-width: 550px;
|
||||
min-height: 30px;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
background-color: #f2f4f5;
|
||||
|
||||
margin-bottom: 1em;
|
||||
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.doc-chat-row {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.doc-chat-banner {
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.1);
|
||||
width: max-content;
|
||||
margin: 8px auto;
|
||||
padding: 4px 14px;
|
||||
border-radius: 8px;
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-chat-row-robot {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.doc-chat-box {
|
||||
color: #000000de;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
max-width: 55%;
|
||||
border-radius: .5rem;
|
||||
padding: .4rem .6rem;
|
||||
margin: .4rem .8rem;
|
||||
background-color: #fff;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.doc-chat-box:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: auto;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
color: #fff;
|
||||
border: 0 solid transparent;
|
||||
border-bottom: 7px solid;
|
||||
border-radius: 0 0 8px 0;
|
||||
left: calc(100% - 4px);
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.doc-chat-box-robot:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: calc(100% - 4px);
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
color: #fff;
|
||||
border: 0 solid transparent;
|
||||
border-bottom: 7px solid;
|
||||
border-radius: 0 0 0 8px;
|
||||
left: auto;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.doc-chat-avatar {
|
||||
background-color: aquamarine;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
border-radius: 18px;
|
||||
}
|
||||
</style>
|
||||
188
docs/.vuepress/config.js
Normal file
@@ -0,0 +1,188 @@
|
||||
module.exports = {
|
||||
title: '炸毛框架',
|
||||
description: '一个聊天机器人 + Web 框架',
|
||||
theme: 'antdocs',
|
||||
markdown: {
|
||||
lineNumbers: true
|
||||
},
|
||||
head: [
|
||||
['link', { rel: 'icon', href: '/logo_trans.png' }],
|
||||
['script', {}, `
|
||||
var _hmt = _hmt || [];
|
||||
(function () {
|
||||
var hm = document.createElement("script");
|
||||
hm.src = "https://hm.baidu.com/hm.js?f0f276cefa10aa31a20ae3815a50b795";
|
||||
var s = document.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(hm, s);
|
||||
})();
|
||||
`]
|
||||
],
|
||||
themeConfig: {
|
||||
repo: 'zhamao-robot/zhamao-framework',
|
||||
logo: '/logo_trans.png',
|
||||
docsDir: 'docs',
|
||||
editLinks: true,
|
||||
lastUpdated: '上次更新',
|
||||
activeHeaderLinks: false,
|
||||
nav: [
|
||||
{ text: '指南', link: '/guide/' },
|
||||
{ text: '事件和注解', link: '/event/' },
|
||||
{ text: '组件', link: '/component/' },
|
||||
{ text: '进阶', link: '/advanced/' },
|
||||
{ text: 'FAQ', link: '/faq/' },
|
||||
{ text: '更新日志', link: '/update/v2/' },
|
||||
{ text: '炸毛框架 v1', link: 'https://docs-v1.zhamao.xin/' }
|
||||
],
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
{
|
||||
title: '指南',
|
||||
collapsable: false,
|
||||
sidebarDepth: 1,
|
||||
children: [
|
||||
'',
|
||||
'installation',
|
||||
'quickstart-robot',
|
||||
'quickstart-http',
|
||||
'onebot-choose',
|
||||
'basic-config',
|
||||
'write-module',
|
||||
'register-event',
|
||||
'upgrade',
|
||||
'errcode'
|
||||
]
|
||||
}
|
||||
],
|
||||
'/event/': [
|
||||
{
|
||||
title: '事件和注解',
|
||||
collapsable: false,
|
||||
sidebarDepth: 1,
|
||||
children: [
|
||||
'',
|
||||
'robot-annotations',
|
||||
'route-annotations',
|
||||
'framework-annotations',
|
||||
'middleware',
|
||||
'custom-annotations',
|
||||
'event-dispatcher'
|
||||
]
|
||||
}
|
||||
],
|
||||
'/component/': [
|
||||
'',
|
||||
{
|
||||
title: '聊天机器人组件',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'bot/robot-api-12',
|
||||
'bot/robot-api',
|
||||
'bot/cqcode',
|
||||
'bot/message-util',
|
||||
'bot/access-token',
|
||||
'bot/turing-api',
|
||||
'bot/help-generator.md',
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '存储组件',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'store/light-cache',
|
||||
'store/mysql',
|
||||
'store/mysql-db',
|
||||
'store/redis',
|
||||
'store/atomics',
|
||||
'store/spin-lock',
|
||||
'store/data-provider'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '通用组件',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'common/context',
|
||||
'common/coroutine-pool',
|
||||
'common/singleton-trait',
|
||||
'common/zmutil',
|
||||
'common/global-functions',
|
||||
'common/console',
|
||||
'common/task-worker',
|
||||
'common/remote-terminal',
|
||||
'common/event-tracer'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'HTTP 组件',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'http/zmrequest',
|
||||
'http/route-manager'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '模块/插件管理',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'module/module-pack',
|
||||
'module/module-unpack'
|
||||
]
|
||||
}
|
||||
],
|
||||
'/advanced/': [
|
||||
'',
|
||||
{
|
||||
title: '框架高级开发',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'framework-structure',
|
||||
'custom-start',
|
||||
'manually-install',
|
||||
'inside-class',
|
||||
'multi-process',
|
||||
'task-worker'
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '开发实战教程',
|
||||
collapsable: true,
|
||||
sidebarDepth: 2,
|
||||
children: [
|
||||
'connect-ws-client',
|
||||
'example/admin',
|
||||
'example/integrate-qingyunke-chatbot',
|
||||
'example/weather-bot'
|
||||
]
|
||||
},
|
||||
],
|
||||
'/faq/': [
|
||||
'',
|
||||
'to-v2',
|
||||
'usual-question',
|
||||
'address-already-in-use',
|
||||
'display-deadlock',
|
||||
'light-cache-wrong',
|
||||
'wait-message-cqbefore'
|
||||
],
|
||||
'/update/': [
|
||||
{
|
||||
title: '更新日志',
|
||||
collapsable: true,
|
||||
sidebarDepth: 0,
|
||||
children: [
|
||||
'v2',
|
||||
'v1',
|
||||
'build-update'
|
||||
]
|
||||
},
|
||||
'config'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
docs/.vuepress/public/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
framework.zhamao.me
|
||||
BIN
docs/.vuepress/public/index_demo.png
Normal file
|
After Width: | Height: | Size: 449 KiB |
BIN
docs/.vuepress/public/logo_trans.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
1
docs/.vuepress/public/robots.txt
Normal file
@@ -0,0 +1 @@
|
||||
User-agent: *
|
||||
1
docs/.vuepress/styles/palette.less
Normal file
@@ -0,0 +1 @@
|
||||
@accentColor: #3665aa;
|
||||
@@ -1 +0,0 @@
|
||||
# FAQ
|
||||
40
docs/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
home: true
|
||||
heroImage: ./logo_trans.png
|
||||
actionBtn:
|
||||
text: 快速上手
|
||||
link: /guide/
|
||||
type: primary
|
||||
size: large
|
||||
features:
|
||||
- title: 高性能
|
||||
details: 基于 PHP 的 Swoole 高性能扩展,利用 WebSocket 进行与 OneBot 协议兼容的聊天机器人软件的通信,还有数据库连接池、内存缓存、多任务进程等特色,大幅增强性能。
|
||||
- title: 易于开发
|
||||
details: 所有功能采用模块化设计,除特殊情况外几乎所有功能都不需要修改框架内任意代码,框架采用灵活的注解进行各类事件绑定,同时支持下断点调试。
|
||||
- title: 接口直观
|
||||
details: 支持命令、普通文本、正则匹配、自然语言处理等多种对话解析方式,利用协程巧妙实现了直观的交互式会话模式,同时支持多种富文本的处理。
|
||||
footer: |
|
||||
Apache-2.0 Licensed | Copyright © 2019-2022 Zhamao Developer Team | <a href="http://beian.miit.gov.cn">沪ICP备2021010446号-1</a>
|
||||
---
|
||||
|
||||
# 快速上手
|
||||
|
||||
## 安装框架和环境
|
||||
|
||||
此命令可一键以模板安装 `zhamao-framework` 和 `PHP` 独立环境到目录下,无需 root!(仅限 Linux)
|
||||
|
||||
```bash
|
||||
mkdir my-app
|
||||
cd my-app
|
||||
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
|
||||
```
|
||||
|
||||
## 运行框架
|
||||
|
||||
```bash
|
||||
./zhamao server
|
||||
```
|
||||
|
||||
## 效果图
|
||||
|
||||

|
||||
4
docs/advanced/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# 进阶开发
|
||||
在本章,下面的部分将详细说明一些具体的案例和自定义框架的操作。
|
||||
|
||||
> 更多进阶教程敬请期待....(或者你可以选择提 Issue 到框架 GitHub,有需求就写入文档)
|
||||
@@ -11,9 +11,11 @@
|
||||
|
||||
以上两种方式,`Header` 方式比 `GET` 方式优先级要高,如果两者均没有指定,框架会将此连接当作 `default` 类型接入。
|
||||
|
||||
!!! note "提示"
|
||||
::: tip 提示
|
||||
|
||||
对于对接 OneBot 标准的机器人客户端,只要符合 OneBot 标准,即 `X-Client-Role` 会自动带上 `universal`、`qq` 等字样,就会自动标记为 `qq` 类型。
|
||||
对于对接 OneBot 标准的机器人客户端,只要符合 OneBot 标准,即 `X-Client-Role` 会自动带上 `universal`、`qq` 等字样,就会自动标记为 `qq` 类型。
|
||||
|
||||
:::
|
||||
|
||||
## 逻辑编写
|
||||
|
||||
@@ -140,4 +142,3 @@ public function onMessage() {
|
||||
```php
|
||||
server()->close($conn->getFd());
|
||||
```
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
从前面的几章中,我们了解到框架有多种下载到本地的方式。
|
||||
|
||||
- Composer 依赖模式
|
||||
- Starter 从模板创建模式
|
||||
- Starter 从模板创建模式(等同于 Composer 模式)
|
||||
- 源码模式
|
||||
- Phar Composer 依赖模式
|
||||
- Phar 源码模式
|
||||
|
||||
### Composer 依赖模式
|
||||
|
||||
@@ -85,17 +87,20 @@ bin/start server # 通过源码模式启动框架
|
||||
- `--debug-mode`:启用调试模式,调试模式的作用是关闭一键协程化和终端交互,减少 Swoole 本身对代码逻辑的干扰(比如执行 `shell_exec()` 报错的话可以开启这个进行调试)。
|
||||
- `--log-{mode}`:设置 log 等级。支持 `--log-debug`,`--log-verbose`,`--log-info`,`--log-warning`,`--log-error`。
|
||||
- `--log-theme`:设置终端信息的主题。这个选项适用于多种终端信息显示的兼容,例如白色终端和不支持颜色的终端。详见 [Console - 主题设置](/component/console/#_2)。
|
||||
- `--disable-console-input`:关闭终端交互,如果你使用的不是 tmux、screen 而是直接将进程使用 systemd 等方式运行到 init 守护进程下,则需要关闭终端交互输入,关闭后不可以使用 `stop, reload, logtest` 等交互命令。
|
||||
- `--disable-coroutine`:关闭一键协程化。
|
||||
- `--remote-terminal`:开启 nc 远程终端,配置文件使用全局中的 `remote_terminal` 项。也可以在全局配置中常开启(status 设置为 true)。
|
||||
- `--daemon`:以守护进程方式运行框架,此参数将直接在输出 motd 后将进程挂到 init 下运行,后台常驻。
|
||||
- `--watch`:监控 `src/` 目录下的文件变化,有变化则自动重新载入代码。开启监控需要安装 PHP 扩展:inotify。使用 pecl 就可以安装:`pecl install inotify`。
|
||||
- `--watch`:监控 `src/` 目录下的文件变化,有变化则自动重新载入代码。开启监控需要安装 PHP 扩展:inotify。使用 pecl 就可以安装:`pecl install inotify`。(注:不支持 WSL 和 macOS)
|
||||
- `--env`:设置运行环境,设置运行环境后将优先加载指定环境的配置文件,支持 `--env=production`,`--env=staging`,`--env=development`,见 [基本配置](/guide/basic-config/#_2)。
|
||||
- `--worker-num`:指定运行的工作进程数量(并不是越多越好,框架默认为 CPU 核心数),例如 `--worker-num=8`。
|
||||
- `--task-worker-num`:启用 TaskWorker 进程并指定数量。
|
||||
- `--show-php-ver`:在启动时显示 Swoole 和 PHP 的版本。
|
||||
|
||||
## 守护进程操作命令
|
||||
|
||||
守护进程在 2.2.0 版本开始,可以使用命令行快速操作,如重启、停止、查看状态等。
|
||||
|
||||
注意,这里的守护进程操作命令是指 **使用 `--daemon` 方式启动的框架**,如使用 Docker、screen、tmux 等方式挂后台跑则此命令不可用!
|
||||
注意,这里的守护进程操作命令是指 **使用 `--daemon` 方式启动的框架**,如使用 Docker、screen、tmux、systemd 等方式挂后台跑则此命令不可用!
|
||||
|
||||
```bash
|
||||
vendor/bin/start daemon:status # 查看守护进程的状态
|
||||
@@ -116,4 +121,16 @@ vendor/bin/start simple-http-server your-web-dir/ --host=0.0.0.0 --port=8080
|
||||
```
|
||||
|
||||
- `your-web-dir` 是必填的参数。
|
||||
- `--host` 和 `--port` 是可选参数,如果不填,则默认使用 `global.php` 配置文件中的配置。
|
||||
- `--host` 和 `--port` 是可选参数,如果不填,则默认使用 `global.php` 配置文件中的配置。
|
||||
|
||||
### 检查配置是否更新
|
||||
|
||||
默认情况下(非源码模式),你可以使用命令 `vendor/bin/start check:config` 来检查你的配置文件是否需要更新部分段落。
|
||||
|
||||
### systemd 配置文件生成器
|
||||
|
||||
框架支持生成 systemd 配置文件 `zhamao.service`,生成后将文件放入 `/etc/systemd/system` 后输入 `systemctl enable zhamao.service` 即可。
|
||||
|
||||
命令:`vendor/bin/start systemd:generate`
|
||||
|
||||
注意,systemd 启动的守护进程模式和使用参数 `--daemon` 不一样,请勿同时混用,直接使用上述命令生成的配置文件即可正常使用!
|
||||
|
||||
@@ -87,14 +87,13 @@ class AdminMiddleware implements MiddlewareInterface
|
||||
*/
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我是管理员
|
||||
) 禁言 1234567 600
|
||||
( 禁言成功!
|
||||
^ 假设我不在管理员名单里
|
||||
) 禁言 1234567 900
|
||||
^ 机器人没有回复,因为中间件返回了 false,不继续执行
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'禁言 1234567 600'},
|
||||
{type:1,content:'禁言成功!'},
|
||||
{type:2,content:'假设我不在管理员名单里'},
|
||||
{type:0,content:'禁言 1234567 900'},
|
||||
{type:2,content:'机器人没有回复,因为中间件返回了 false,不继续执行'},
|
||||
]"></chat-box>
|
||||
|
||||
而这时候有朋友又要问了,如果我有一系列管理员命令,假设都在一个叫 `AdminFunc.php` 的模块类里,我是不是还得一个一个地给注解事件写 `@Middleware("admin")` 呢?当然不需要!如果你这个类所有的注解事件都是机器人的聊天事件(`@CQCommand`,`@CQMessage`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。
|
||||
|
||||
@@ -120,112 +119,111 @@ class AdminFunc
|
||||
|
||||
上面我们讲到了,中间件里面使用了 `LightCache` 轻量缓存来储存临时的管理员列表,那么我们将这部分的代码完善吧!
|
||||
|
||||
=== "src/Module/Example/AdminFunc.php"
|
||||
**src/Module/Example/AdminFunc.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\API\CQ;
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\API\CQ;
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
|
||||
/**
|
||||
* Class AdminFunc
|
||||
* @package Module\Example
|
||||
* @CQCommand(match="解除禁言",message_type="group")
|
||||
*/
|
||||
public function unbanSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0);
|
||||
return "解除禁言成功!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**src/Module/Example/AdminManager.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
|
||||
class AdminManager
|
||||
{
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
if (!LightCache::isset("admin_list")) { //一次性代码,首次执行才会执行if
|
||||
LightCache::set("admin_list", [ // 框架启动时初始化管理员列表
|
||||
"123456",
|
||||
"234567"
|
||||
], -2); // 这里用 -2 的原因是将这一列表持久化保存,避免关闭框架后丢失
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="添加管理员")
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
class AdminFunc
|
||||
{
|
||||
/**
|
||||
* @CQCommand(match="禁言",message_type="group")
|
||||
*/
|
||||
public function banSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$r2 = ctx()->getFullArg("请输入禁言的时间(秒)");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, $r2);
|
||||
return "禁言成功!";
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="解除禁言",message_type="group")
|
||||
*/
|
||||
public function unbanSomeone() {
|
||||
$r1 = ctx()->getNextArg("请输入禁言的人或at他");
|
||||
$cq = CQ::getCQ($r1);
|
||||
if ($cq !== null) {
|
||||
if ($cq["type"] != "at") return "请at或者输入正确的QQ号!";
|
||||
$r1 = $cq["params"]["qq"];
|
||||
}
|
||||
// 群内禁言用户
|
||||
ctx()->getRobot()->setGroupBan(ctx()->getGroupId(), $r1, 0);
|
||||
return "解除禁言成功!";
|
||||
}
|
||||
public function addAdmin() { //只有管理员才能添加管理员
|
||||
$qq = ctx()->getNextArg("请输入要添加管理员的QQ(qq号码,不可at)");
|
||||
SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
$ls = LightCache::get("admin_list");
|
||||
if (!in_array($qq, $ls)) $ls[] = $qq;
|
||||
LightCache::set("admin_list", $ls, -2);
|
||||
SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
return "成功添加 $qq 到管理员列表!";
|
||||
}
|
||||
```
|
||||
|
||||
=== "src/Module/Example/AdminManager.php"
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
use ZM\Annotation\Http\Middleware;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
use ZM\Store\LightCache;
|
||||
use ZM\Store\Lock\SpinLock;
|
||||
|
||||
class AdminManager
|
||||
{
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
if (!LightCache::isset("admin_list")) { //一次性代码,首次执行才会执行if
|
||||
LightCache::set("admin_list", [ // 框架启动时初始化管理员列表
|
||||
"123456",
|
||||
"234567"
|
||||
], -2); // 这里用 -2 的原因是将这一列表持久化保存,避免关闭框架后丢失
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @CQCommand(match="添加管理员")
|
||||
* @Middleware("admin")
|
||||
*/
|
||||
public function addAdmin() { //只有管理员才能添加管理员
|
||||
$qq = ctx()->getNextArg("请输入要添加管理员的QQ(qq号码,不可at)");
|
||||
SpinLock::lock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
$ls = LightCache::get("admin_list");
|
||||
if (!in_array($qq, $ls)) $ls[] = $qq;
|
||||
LightCache::set("admin_list", $ls, -2);
|
||||
SpinLock::unlock("admin_list"); //如果是多进程模式的话需要加锁
|
||||
return "成功添加 $qq 到管理员列表!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 现在我是 123456
|
||||
) 禁言 13579 60
|
||||
( 禁言成功!
|
||||
) 解除禁言 13579
|
||||
( 解除禁言成功!
|
||||
) 添加管理员 98765
|
||||
( 成功添加 98765 到管理员列表!
|
||||
^ 现在我是98765
|
||||
) 禁言 13579
|
||||
( 请输入禁言的时间(秒)
|
||||
) 120
|
||||
( 禁言成功!
|
||||
</chat-box>
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'现在我是 123456'},
|
||||
{type:0,content:'禁言 13579 60'},
|
||||
{type:1,content:'禁言成功!'},
|
||||
{type:0,content:'解除禁言 13579'},
|
||||
{type:1,content:'解除禁言成功!'},
|
||||
{type:0,content:'添加管理员 98765'},
|
||||
{type:1,content:'成功添加 98765 到管理员列表!'},
|
||||
{type:2,content:'现在我是98765'},
|
||||
{type:0,content:'禁言 13579'},
|
||||
{type:1,content:'请输入禁言的时间(秒)'},
|
||||
{type:0,content:'120'},
|
||||
{type:1,content:'禁言成功!'},
|
||||
]"></chat-box>
|
||||
|
||||
85
docs/advanced/example/integrate-qingyunke-chatbot.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# 接入青云客智能聊天机器人API
|
||||
|
||||
作为一个群聊机器人,懂得聊天会让机器人增色不少,在大数据和 AI 热潮下,不少厂商都研发了自己的智能聊天 API,例如图灵机器人、腾讯智能闲聊等,大厂开发的 API 自然有着他人无可比拟的健壮性和可靠性,但是随之而来不菲的价格显然并不适合大众开发者。这时候一个免费、可用的智能聊天 API 便非常重要了,其中,青云客是少有的完全免费、无需注册的智能聊天 API,提供了包括智能聊天、歌词、天气查询、笑话等多种有用功能,且接入简单,非常适合新手开发者尝试。
|
||||
|
||||
## 结果演示
|
||||
|
||||

|
||||
|
||||
## 阅读接入指南
|
||||
|
||||
不管接入何种服务,阅读接入指南永远都是最优先、最重要的一步,所幸青云客的接入指南十分简单,简单来说归纳为以下:
|
||||
* 请求: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;
|
||||
}
|
||||
```
|
||||
113
docs/advanced/example/weather-bot.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 基于词性分析和魅族天气的天气查询机器人
|
||||
|
||||
本文将基于 [`jieba-php`](https://github.com/fukuball/jieba-php) 中文分词库以及 [魅族天气 API](https://github.com/shichunlei/-Api/blob/master/MeizuWeather.md) 开发一个天气查询机器人。
|
||||
|
||||
## 结果演示
|
||||
|
||||

|
||||
尾部的随机表情并非本教程的一部分。
|
||||
|
||||
## 逻辑编写
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
## 框架运行总结构图
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# 进阶开发
|
||||
在本章,下面的部分将详细说明一些具体的案例和自定义框架的操作。
|
||||
|
||||
- 如何自定义修改框架本身?- [框架启动方式](/advanced/custom-start/)
|
||||
- 如何接入一个自己的 WebSocket 客户端?- [接入 WebSocket 客户端](/advanced/connect-ws-client/)
|
||||
- 框架到底是怎么工作的?- [框架结构剖析](/advanced/framework-structure/)
|
||||
|
||||
> 更多进阶教程敬请期待....(或者你可以选择提 Issue 到框架 GitHub,有需求就写入文档)
|
||||
|
||||
@@ -27,3 +27,41 @@
|
||||
TODO:先放一放。
|
||||
```
|
||||
|
||||
## ZM\Entity\MatchObject
|
||||
|
||||
此类是调用方法 `MessageUtil::matchCommand()` 返回的对象体,含有匹配成功与否和匹配到的注解相关的信息。
|
||||
|
||||
### 属性
|
||||
|
||||
- `$match`:`bool` 类型,返回匹配是否成功
|
||||
- `$object`:`CQCommand` 注解类,如果匹配成功则返回对应的 `@CQCommand` 信息
|
||||
- `match`:`array` 类型,如果匹配成功则返回匹配到的参数
|
||||
|
||||
```php
|
||||
// 假设我有一个注解事件 @CQCommand(match="你好"),绑定的函数是 \Module\Example\Hello 下的 hello123()
|
||||
|
||||
$obj = MessageUtil::matchCommand("你好 我叫顺溜 我今年二十八", ctx()->getData());
|
||||
/* 以下是返回信息,仅供参考
|
||||
$obj->match ==> true
|
||||
$obj->object ==> \ZM\Annotation\CQ\CQCommand: (
|
||||
match: "你好",
|
||||
pattern: "",
|
||||
regex: "",
|
||||
start_with: "",
|
||||
end_with: "",
|
||||
keyword: "",
|
||||
alias: [],
|
||||
message_type: "",
|
||||
user_id: 0,
|
||||
group_id: 0,
|
||||
discuss_id: 0,
|
||||
level: 20,
|
||||
method: "hello123",
|
||||
class: \Module\Example\Hello::class
|
||||
)
|
||||
$obj->match ==> [
|
||||
"我叫顺溜",
|
||||
"我今年二十八"
|
||||
]
|
||||
*/
|
||||
```
|
||||
|
||||
3
docs/advanced/manually-install.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 手动部署环境教程
|
||||
|
||||
TODO: 还没写
|
||||
@@ -16,13 +16,13 @@ PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采
|
||||
|
||||
**但是**,CPU 密集型的应用怎么办呢?假设我的 Web 应用有大量的排序、md5 运算怎么办呢?这样的阻塞,假设是一个超级大的 for 循环或者是要执行很长时间的 while 循环,CPU 一直在被占用。多进程就是针对 CPU 密集型的应用说 yes 的一个方案。
|
||||
|
||||

|
||||

|
||||
|
||||
我们假设现在有 3 个请求同时访问,也就是说上面的流程需要执行 3 遍。而如果我们只有一个进程的话,最后一个请求需要等待的时间为 `2*3+5*3=21` 秒,非常耗时。
|
||||
|
||||
而如果有两个进程处理 3 个请求,则最后一个完成的请求就缩短了,`2+5+2+5=14` 秒。
|
||||
|
||||
.png)
|
||||

|
||||
|
||||
所以如果要充分利用你的服务器或者个人电脑的多核 CPU 资源,就要设置多个进程来处理。一个进程只能在一个 CPU 上运行,而设置了多进程后,就可以让多核 CPU 充分运行多个进程,所以我们给框架设置多进程的推荐数值为等同于 CPU 的核心数。
|
||||
|
||||
@@ -32,7 +32,7 @@ PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采
|
||||
|
||||
## 框架进程模型
|
||||
|
||||
.png)
|
||||

|
||||
|
||||
上图中,横向的时间片可以理解为并行执行,这些操作在多个 CPU 内可能同时在执行。
|
||||
|
||||
@@ -40,7 +40,7 @@ PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采
|
||||
|
||||
众所周知,进程是程序在操作系统中的一个边界,和自己有关的一切变量、内容和代码都在自己的进程内,不同进程之间如果不使用管道等方式,是不可以互相访问的。而加上开始描述的,创建子进程是一个复制自身的过程,所以也就会有如下图的情况:
|
||||
|
||||
.png)
|
||||

|
||||
|
||||
我们以静态类为例,设置一个进程中的全局变量。这里就会出现,同一个静态变量在多个进程中完全不同的值的结果。此后,我们将会在 Worker 进程中执行用户的代码,如果设置 Worker 数量仅为 1 的话,那么就简单许多了,你还是可以使用全局变量或静态类来存储你想要的内容而不用担心这种多个进程变量隔离的情况(因为用户的 Web 请求处理的代码只会在一个 Worker 进程中执行)。如果像上图一样设置了多个 Worker,则用户过来的比如 HTTP 请求就有可能出现在不同的 Worker 进程中,给全局变量设值就一定会造成不同步的问题。这时我们就不可以使用全局变量做数据同步(注意,我说的是数据同步)。
|
||||
|
||||
@@ -63,8 +63,10 @@ PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采
|
||||
| LightCache | 框架封装的跨进程的 key-value 存储模型 | 性能强悍,无 I/O 和网络通信 | 需要提前分配最大内存大小,最大单个值长度大小,不灵活 |
|
||||
| WorkerCache | 框架封装的基于进程的 key-value 存储模型,类似 Redis | 无需提前分配最大内存大小,受限于 PHP memory_limit | 见 WorkerCache 的说明 |
|
||||
|
||||
!!! note "WorkerCache 的说明"
|
||||
对于 WorkerCache 来说,其实是比较特殊的进程间通信。具体来说就是,WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
|
||||
|
||||
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。
|
||||
::: tip WorkerCache 的说明
|
||||
|
||||
对于 WorkerCache 来说,其实是比较特殊的进程间通信。具体来说就是,WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
|
||||
|
||||
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。
|
||||
|
||||
:::
|
||||
|
||||
3
docs/advanced/php-env.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# PHP 环境高级部署方式
|
||||
|
||||
TODO: 留个坑。
|
||||
3
docs/advanced/task-worker.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 使用 TaskWorker 进程处理密集运算
|
||||
|
||||
> TODO: 新开个坑,有时间补上。(__填坑标记__)
|
||||
@@ -1,103 +0,0 @@
|
||||
|
||||
.md-header-nav__button.md-logo {
|
||||
padding: .2rem;
|
||||
margin: .2rem;
|
||||
}
|
||||
|
||||
.md-header-nav__button.md-logo img, .md-header-nav__button.md-logo svg {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
|
||||
.doc-chat-container {
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
min-height: 30px;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
background-color: var(--md-code-bg-color);
|
||||
padding: 12px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
box-shadow: 0 3px 1px -2px rgba(0,0,0,.2), 0 2px 2px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.doc-chat-row {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.doc-chat-banner {
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.1);
|
||||
width: max-content;
|
||||
margin: 8px auto;
|
||||
padding: 4px 14px;
|
||||
border-radius: 8px;
|
||||
color: gray;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-chat-row-robot {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
.doc-chat-box {
|
||||
color: #000000de;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
max-width: 55%;
|
||||
border-radius: .5rem;
|
||||
padding: .4rem .6rem;
|
||||
margin: .4rem .8rem;
|
||||
background-color: #fff;
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
overflow-wrap: break-word;
|
||||
white-space: normal;
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.doc-chat-box:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: auto;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
color: #fff;
|
||||
border: 0 solid transparent;
|
||||
border-bottom: 7px solid;
|
||||
border-radius: 0 0 8px 0;
|
||||
left: calc(100% - 4px);
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.doc-chat-box-robot:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: calc(100% - 4px);
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
color: #fff;
|
||||
border: 0 solid transparent;
|
||||
border-bottom: 7px solid;
|
||||
border-radius: 0 0 0 8px;
|
||||
left: auto;
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.doc-chat-avatar {
|
||||
background-color: aquamarine;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.md-typeset .admonition, .md-typeset details {
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 10 KiB |
3
docs/component/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 框架组件
|
||||
|
||||
这里列举了框架内的你可能会用到的常用组件。
|
||||
59
docs/component/bot/access-token.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# 接入安全验证 - Token
|
||||
|
||||
为了保障安全,框架支持给接入的 WebSocket 连接验证 Token,如果不设置 Token 同时又将框架的端口暴露在公网将会非常危险。
|
||||
|
||||
炸毛框架兼容 OneBot 标准的机器人客户端,所以自带一个 Token 验证器。
|
||||
|
||||
关于 Access Token 方面的标准规范,请参考下面内容:
|
||||
|
||||
- [OneBot - 鉴权](https://github.com/howmanybots/onebot/blob/master/v11/specs/communication/authorization.md)
|
||||
- [go-cqhttp - 配置](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md)
|
||||
|
||||
> 以 go-cqhttp 举例,如果要设置验证,则将 go-cqhttp 配置文件中的 `access_token` 项填入内容即可。
|
||||
|
||||
## 验证位置
|
||||
|
||||
框架对 Token 的验证是内置的,在事件 `open`(WebSocket 连接接入时)触发。
|
||||
|
||||
如果是兼容 OneBot 标准的客户端接入,则一切都是兼容的。
|
||||
|
||||
如果是自定义的其他 WebSocket 客户端也想接入框架,那么其他 WebSocket 客户端也需要进行相应的设置才能利用此 Token 验证。
|
||||
|
||||
如果验证成功(Token 符合要求)则分发事件 `@OnOpenEvent`,否则此事件不触发,同时断开 WebSocket 连接。
|
||||
|
||||
## 标准验证(字符串形式)
|
||||
|
||||
默认的情况下,在框架的全局配置文件 `global.php` 中,对配置项 `access_token` 填入与 OneBot 客户端相同的 `access_token` 即可实现鉴权。下面是一个最基本的和 go-cqhttp 设置鉴权配置:
|
||||
|
||||
go-cqhttp 的配置段:
|
||||
|
||||
```
|
||||
// 访问密钥, 强烈推荐在公网的服务器设置
|
||||
access_token: "emhhbWFvLXJvYm90"
|
||||
```
|
||||
|
||||
框架的配置文件配置段:
|
||||
|
||||
```php
|
||||
/** onebot连接约定的token */
|
||||
$config["access_token"] = 'emhhbWFvLXJvYm90';
|
||||
```
|
||||
|
||||
然后重启框架和 go-cqhttp 即可。(其他 OneBot 客户端同理)
|
||||
|
||||
## 自定义验证(Token 验证)
|
||||
|
||||
有些情况下,使用一个单一的字符串可能无法满足你对 Token 验证的安全需求,需要自定义一些判断模式才能满足,所以框架的 `access_token` 配置项支持动态的闭包函数自行编写判断逻辑,例如下面的一个例子,我可以让框架同时允许接入多个不同 token 的 WebSocket 连接:
|
||||
|
||||
```php
|
||||
/** onebot连接约定的token */
|
||||
$config["access_token"] = function($token){
|
||||
$allow = ['emhhbWFvLXJvYm90','aXMtdmVyeS1nb29k'];
|
||||
if (in_array($token, $allow)) return true;
|
||||
else return false;
|
||||
};
|
||||
```
|
||||
|
||||
## 自定义验证(open 事件)
|
||||
|
||||
当然,这里设置了自定义方式,其实你也可以在下一层的 `@OnOpenEvent` 注解事件中进行自定义内容和判断,具体见 `@OnOpenEvent` 的相关章节。
|
||||
@@ -1,4 +1,4 @@
|
||||
# CQ 码(多媒体消息)
|
||||
# 多媒体消息 - CQ码
|
||||
|
||||
消息中的多媒体内容使用 CQ 码来表示,形如 `[CQ:face,id=178]`。其中,`[CQ:]` 是固定格式;`face` 是「功能名」,除了 `face` 还有许多不同的功能名;`id=178` 是「参数」,某些功能不需要参数,而另一些需要多个参数,当有多个参数时,参数间使用逗号分隔。
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
更多 CQ 码功能请参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md)。
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
CQ 码中不应有多余的空格,例如不应该使用 `[CQ:face, id=178]`。
|
||||
CQ 码中不应有多余的空格,例如不应该使用 `[CQ:face, id=178]`。
|
||||
|
||||
CQ 码的参数值可以包含空格、换行、除 `[],&` 之外的特殊符号等。在解析时,应直接取 `[CQ:` 后、第一个 `,` 或 `]` 前的部分为功能名,第一个 `,` 之后到 `]` 之间的部分为参数,按 `,` 分割后,每个部分第一个 `=` 前的内容为参数名,之后的部分为参数值。例如 `[CQ:share,title=标题中有=等号,url=http://baidu.com]` 中,功能名为 `share`,`title` 参数值为 `标题中有=等号`,`url` 参数值为 `http://baidu.com`。
|
||||
CQ 码的参数值可以包含空格、换行、除 `[],&` 之外的特殊符号等。在解析时,应直接取 `[CQ:` 后、第一个 `,` 或 `]` 前的部分为功能名,第一个 `,` 之后到 `]` 之间的部分为参数,按 `,` 分割后,每个部分第一个 `=` 前的内容为参数名,之后的部分为参数值。例如 `[CQ:share,title=标题中有=等号,url=http://baidu.com]` 中,功能名为 `share`,`title` 参数值为 `标题中有=等号`,`url` 参数值为 `http://baidu.com`。
|
||||
|
||||
:::
|
||||
|
||||
## 转义
|
||||
|
||||
@@ -69,12 +71,12 @@ class Hello {
|
||||
}
|
||||
```
|
||||
|
||||
效果
|
||||
效果:
|
||||
|
||||
<chat-box>
|
||||
) 发送图片
|
||||
[ https://zhamao.xin/file/hello.jpg
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'发送图片'},
|
||||
{type:3,content:'https://zhamao.xin/file/hello.jpg'}
|
||||
]"></chat-box>
|
||||
|
||||
## CQ 码操作
|
||||
|
||||
@@ -128,8 +130,9 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
|
||||
|
||||
解析 CQ 码。
|
||||
|
||||
- 参数:`getCQ($msg);`:要解析出 CQ 码的消息。
|
||||
- 返回:`数组 | null`,见下表
|
||||
- 定义:`getCQ($msg, $is_object = false)`
|
||||
- 参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject` 对象,此对象的属性和下表相同。(2.3.0+ 版本可用)
|
||||
- 返回:`数组 | CQObject | null`,见下表。
|
||||
|
||||
| 键名 | 说明 |
|
||||
| ------ | ------------------------------------------------------------ |
|
||||
@@ -140,6 +143,10 @@ $str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]"
|
||||
|
||||
### CQ::getAllCQ()
|
||||
|
||||
定义:`CQ::getAllCQ($msg, $is_object = false)`
|
||||
|
||||
参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject[]` 对象数组,此对象的属性和上面的表格内相同。(2.3.0+ 版本可用)
|
||||
|
||||
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
|
||||
|
||||
```php
|
||||
@@ -174,7 +181,7 @@ CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
|
||||
|
||||
定义:`CQ::face($id)`
|
||||
|
||||
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](/assets/face_id.html)。
|
||||
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](https://static.zhamao.me/face_id.html)。
|
||||
|
||||
```php
|
||||
/**
|
||||
@@ -186,15 +193,17 @@ public function faceTest() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 打盹
|
||||
( 正在打盹...
|
||||
[ https://docs-v1.zhamao.xin/face/8.gif
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'打盹'},
|
||||
{type:1,content:'正在打盹...'},
|
||||
{type:3,content:'https://docs-v1.zhamao.xin/face/8.gif'},
|
||||
]"></chat-box>
|
||||
|
||||
!!! note "提示"
|
||||
对于不断更新的 QQ 版本下,可能会持续扩充新的 QQ 表情,如果上表没有新的表情的话,也可以使用消息接收的方式,让机器人收到表情后解析出来对应的 id 然后再发送。
|
||||
::: tip 提示
|
||||
|
||||
对于不断更新的 QQ 版本下,可能会持续扩充新的 QQ 表情,如果上表没有新的表情的话,也可以使用消息接收的方式,让机器人收到表情后解析出来对应的 id 然后再发送。
|
||||
|
||||
:::
|
||||
|
||||
### CQ::image() - 发送图片
|
||||
|
||||
@@ -216,7 +225,11 @@ public function faceTest() {
|
||||
|
||||
- 绝对路径,例如 `file:///root/imagetest/1.png`,格式使用 [`file` URI](https://tools.ietf.org/html/rfc8089)
|
||||
- 网络 URL,例如 `http://i1.piimg.com/567571/fdd6e7b6d93f1ef0.jpg`
|
||||
- Base64 编码,例如 `base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==`
|
||||
- Base64 编码,例如
|
||||
|
||||
```
|
||||
base64://iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAIAAADJt1n/AAAAKElEQVQ4EWPk5+RmIBcwkasRpG9UM4mhNxpgowFGMARGEwnBIEJVAAAdBgBNAZf+QAAAAABJRU5ErkJggg==
|
||||
```
|
||||
|
||||
### CQ::record() - 发送语音
|
||||
|
||||
@@ -245,10 +258,10 @@ public function say() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 说你好
|
||||
( [语音消息,点击收听] 2'' )))
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'说你好'},
|
||||
{type:1,content:'[语音消息,点击收听] 2\'\' )))'},
|
||||
]"></chat-box>
|
||||
|
||||
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
|
||||
|
||||
@@ -269,10 +282,10 @@ public function atTest() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) at测试
|
||||
( @鲸鱼 你好啊!
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'at测试'},
|
||||
{type:1,content:'@鲸鱼 你好啊!'},
|
||||
]"></chat-box>
|
||||
|
||||
### CQ::video() - 发送短视频
|
||||
|
||||
@@ -341,9 +354,11 @@ public function atTest() {
|
||||
|
||||
匿名发消息。需要在允许匿名发消息的群里发。
|
||||
|
||||
!!! tip "提示"
|
||||
::: tip 提示
|
||||
|
||||
当收到匿名消息时,需要通过 [消息事件的群消息](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#群消息) 的 `anonymous` 字段判断。
|
||||
当收到匿名消息时,需要通过 [消息事件的群消息](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#群消息) 的 `anonymous` 字段判断。
|
||||
|
||||
:::
|
||||
|
||||
定义:`CQ::anonymous($ignore = 1)`
|
||||
|
||||
@@ -510,9 +525,11 @@ public function xmlTest() {
|
||||
|
||||
其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0,走小程序通道,填了走富文本通道发送。
|
||||
|
||||
!!! tip "提示"
|
||||
::: tip 提示
|
||||
|
||||
因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
|
||||
因为某些众所周知的原因,XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
|
||||
|
||||
:::
|
||||
|
||||
### CQ::_custom() - 扩展自定义 CQ 码
|
||||
|
||||
@@ -531,4 +548,3 @@ public function xmlTest() {
|
||||
CQ::_custom("at",["qq" => "123456","qwe" => "asd"]);
|
||||
// 返回:[CQ:at,qq=123456,qwe=asd]
|
||||
```
|
||||
|
||||
35
docs/component/bot/help-generator.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# 命令帮助生成器 - CommandHelpGenerator
|
||||
|
||||
类定义:`\ZM\Utils\MessageUtil`
|
||||
目前包含在 `MessageUtil` 类中,日后可能会进行拆分。
|
||||
|
||||
> 2.7.3 版本起可用。
|
||||
|
||||
## 方法
|
||||
|
||||
### generateCommandHelp
|
||||
|
||||
自动扫描定义的所有命令,生成注解树,并以此生成命令列表及帮助。
|
||||
|
||||
第一次运行时,会遍历一遍注解树并进行生成,此后会从缓存中读取。
|
||||
|
||||
定义:`generateCommandHelp()`
|
||||
|
||||
返回值:`array` 每个元素对应一个命令的帮助信息,格式为:`命令名(其他触发条件):命令描述`
|
||||
|
||||
示例:`天气(温度、包含“天气”):查询指定城市的天气`
|
||||
|
||||
```php
|
||||
/**
|
||||
* 输出帮助信息
|
||||
*
|
||||
* @CQCommand("帮助")
|
||||
*/
|
||||
#[CQCommand('帮助')]
|
||||
public function help(): string
|
||||
{
|
||||
$helps = MessageUtil::generateCommandHelp();
|
||||
array_unshift($helps, '帮助:');
|
||||
return implode("\n", $helps);
|
||||
}
|
||||
```
|
||||
181
docs/component/bot/message-util.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# 消息处理工具类 - MessageUtil
|
||||
|
||||
类定义:`\ZM\Utils\MessageUtil`
|
||||
|
||||
> 2.3.0 版本起可用。
|
||||
|
||||
这里放置一些机器人聊天消息处理的便捷静态方法,例如下载图片等。
|
||||
|
||||
## 方法
|
||||
|
||||
### downloadCQImage()
|
||||
|
||||
下载用户消息中所带的所有图片,并返回文件路径。
|
||||
|
||||
定义:`downloadCQImage($msg, $path = null)`
|
||||
|
||||
参数 `$msg` 为带图片的用户消息,例如 `你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]`
|
||||
|
||||
参数 `$path` 为图片下载的路径,如果不填(默认 null)则指定为 `zm_data/images/` 目录,且不存在会自动创建。
|
||||
|
||||
```php
|
||||
$r = MessageUtil::downloadCQImage("你好啊!\n[CQ:image,file=a.jpg,url=https://zhamao.xin/file/hello.jpg]");
|
||||
/*
|
||||
$r == [
|
||||
"/path-to/zhamao-framework/zm_data/images/a.jpg"
|
||||
];
|
||||
*/
|
||||
```
|
||||
|
||||
如果返回的是空数组 `[ ]`,则表明消息中没有图片。如果返回的是 `false`,则表明其中至少一张下载失败或路径有误。
|
||||
|
||||
### containsImage()
|
||||
|
||||
检查消息中是否含有图片。
|
||||
|
||||
定义:`containsImage($msg)`
|
||||
|
||||
返回:`bool`,你懂的,true 就是有,false 就没有。
|
||||
|
||||
```php
|
||||
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 码。
|
||||
|
||||
定义:`getImageCQFromLocal($file, $type = 0)`
|
||||
|
||||
参数 `$file` 为图片的绝对路径。
|
||||
|
||||
返回:图片的 CQ 码,如 `[CQ:image,file=xxxxx]`
|
||||
|
||||
参数 `$type`:
|
||||
|
||||
- `0`:以 base64 的方式发送图片,返回结果如 `[CQ:image,file=base64://xxxxxx]`
|
||||
- `1`:以 `file://` 本地文件的方式发送图片,返回结果如 `[CQ:image,file=file:///path-to/images/a.jpg]`
|
||||
- `2`:返回图片的 http:// CQ 码(默认为 /images/ 路径就是文件对应所在的目录),如 `[CQ:image,file=http://127.0.0.1:20001/images/a.jpg]`
|
||||
|
||||
### splitCommand()
|
||||
|
||||
切割用户消息为数组形式(`@CQCommand` 就是使用此方式切割的)
|
||||
|
||||
定义:`splitCommand($msg): array`
|
||||
|
||||
返回:数组,切分后的。
|
||||
|
||||
::: tip 为什么不直接使用 explode 呢
|
||||
|
||||
因为 `explode()` 只会简单粗暴的切割字符串,假设用户输入的消息中两个词中间有多个空格,则会有空的词出现。例如 `你好 我是一个长空格`。此函数会将多个空格当作一个空格来对待。
|
||||
|
||||
:::
|
||||
|
||||
```php
|
||||
MessageUtil::splitCommand("你好 我是傻瓜\n我是傻瓜二号"); // ["你好","我是傻瓜","我是傻瓜二号"]
|
||||
MessageUtil::splitCommand("我有 三个空格"); // ["我有","三个空格"]
|
||||
```
|
||||
|
||||
### matchCommand()
|
||||
|
||||
匹配一条消息到 `@CQCommand` 规则的注解事件,返回要执行的类和函数位置。
|
||||
|
||||
定义:`matchCommand($msg, $obj)`
|
||||
|
||||
参数 `$msg` 为消息内容。
|
||||
|
||||
参数 `$obj` 为事件的对象,可使用 `ctx()->getData()` 获取原先的事件体(仅限 OneBot 消息类型事件中使用)
|
||||
|
||||
返回:`\ZM\Entity\MatchObject` 对象,含有匹配成功与否,匹配到的注解对象,匹配到的分割词等,见 []
|
||||
|
||||
### addShortCommand()
|
||||
|
||||
快速添加一条静态消息回复命令。
|
||||
|
||||
定义:`addShortCommand($command, string $reply)`
|
||||
|
||||
参数 `$command` 为问的内容,如 `炸毛不聪明`。
|
||||
|
||||
参数 `$reply` 为回复的内容,如 `其实还是很聪明的!`。
|
||||
|
||||
这个命令推荐在 `@OnStart` 注解下使用,可以用这个来做一个动态的词库,从文件加载后使用。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
MessageUtil::addShortCommand("炸毛不聪明", "其实还是很聪明的!");
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'炸毛不聪明'},
|
||||
{type:1,content:'其实还是很聪明的!'},
|
||||
]"></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]"
|
||||
```
|
||||
11
docs/component/bot/robot-api-12.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 机器人动作 - V12
|
||||
|
||||
::: tip 提示
|
||||
|
||||
目前由于 OneBot 12 标准还没有定稿,处于草案阶段,故框架暂不更新。
|
||||
|
||||
在未来升级到 OneBot 12 标准后,框架会提供转换及兼容措施以及 12 版本的 API 方法。
|
||||
|
||||
:::
|
||||
|
||||
见 [机器人动作(OneBot 11)](./robot-api.html)。
|
||||
@@ -1,26 +1,29 @@
|
||||
# 机器人 API(ZMRobot)
|
||||
# 机器人动作 - V11
|
||||
|
||||
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,13 +106,14 @@ $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);
|
||||
```
|
||||
|
||||
输出结果
|
||||
|
||||
```json
|
||||
// 输出结果
|
||||
{
|
||||
"status": "ok",
|
||||
"retcode": 0,
|
||||
@@ -158,21 +162,18 @@ vardump($result["retcode"]); //如果成功撤回,输出 int(0)
|
||||
| ------------ | -------------- | ------- |
|
||||
| `message_id` | number (int32) | 消息 ID |
|
||||
|
||||
例子
|
||||
代码
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
$bot = OneBotV11::get(123456); // 123456是你的机器人QQ
|
||||
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
|
||||
```
|
||||
|
||||
```php
|
||||
$bot = ZMRobot::get(123456); // 123456是你的机器人QQ
|
||||
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
|
||||
<chat-box>
|
||||
( 你好啊!你好你好!
|
||||
</chat-box>
|
||||
效果
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:1,content:'你好啊!你好你好!'}
|
||||
]"></chat-box>
|
||||
|
||||
### sendGroupMsg()
|
||||
|
||||
@@ -618,10 +619,11 @@ vardump($result["retcode"]); //如果成功撤回,输出 int(0)
|
||||
|
||||
获取 Cookies。
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
目前开源的 mirai 为底层的机器人客户端均不支持获取 Cookies 和 CSRF Token,包括 go-cqhttp。
|
||||
目前开源的 mirai 为底层的机器人客户端均不支持获取 Cookies 和 CSRF Token,包括 go-cqhttp。
|
||||
|
||||
:::
|
||||
|
||||
参数
|
||||
|
||||
@@ -766,6 +768,23 @@ 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。
|
||||
@@ -787,4 +806,3 @@ $result = $bot->callExtendedAPI("get_group_root_files", ["group_id" => 123456]);
|
||||
var_dump($result["data"]);
|
||||
// 输出群文件列表
|
||||
```
|
||||
|
||||
86
docs/component/bot/turing-api.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 图灵聊天 - TuringAPI
|
||||
|
||||
类定义:`\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 :my-chats="[
|
||||
{type:0,content:'你咋了'},
|
||||
{type:1,content:'我没事哦,谢谢您的关心。'},
|
||||
{type:0,content:'上海天气'},
|
||||
{type:1,content:'上海:周一 03月29日,小雨 东南风转东风,最低气温14度,最高气温24度。'},
|
||||
{type:2,content:'切换为群内'},
|
||||
{type:0,content:'机器人'},
|
||||
{type:1,content:'我在!有什么事吗?'},
|
||||
{type:0,content:'你叫啥'},
|
||||
{type:1,content:'我的名字叫炸毛,认识你很高兴呢!'},
|
||||
]"></chat-box>
|
||||
|
||||
在默认示例模块中的例子是直接可以拿来用的,这段代码同时做了对 at 的处理、以及兼容用户自定义写的其他命令的方式,下面是默认模块填好 apikey 后可以用的各种方式提问:
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'切换为群内'},
|
||||
{type:0,content:'我是一条普通消息,这条机器人不会回复我'},
|
||||
{type:0,content:'@机器人 你叫啥'},
|
||||
{type:1,content:'我是聪明可爱的炸毛,认识你很高兴。'},
|
||||
{type:0,content:'机器人'},
|
||||
{type:1,content:'我在!有什么事吗?'},
|
||||
{type:0,content:'一言'},
|
||||
{type:1,content:'多少事,从来急,天地转,光阴迫,一万年太久,只争朝夕。'},
|
||||
]"></chat-box>
|
||||
@@ -52,10 +52,11 @@ vendor/bin/start server --log-debug # 以 debug 等级启动框架
|
||||
|
||||
输出 warning 级别的 log。
|
||||
|
||||
!!! warning 注意
|
||||
::: warning 注意
|
||||
|
||||
框架内出现的用户态异常,比如无法发送 API、无法连接数据库等错误,都是 warning 错误,不会导致框架崩溃或功能错误的异常情况建议都使用 warning 输出而不是 error。
|
||||
框架内出现的用户态异常,比如无法发送 API、无法连接数据库等错误,都是 warning 错误,不会导致框架崩溃或功能错误的异常情况建议都使用 warning 输出而不是 error。
|
||||
|
||||
:::
|
||||
|
||||
### Console::info()
|
||||
|
||||
@@ -100,9 +101,11 @@ $str = Console::setColor("I am gold color.", "gold");
|
||||
|
||||
炸毛框架支持从终端输入命令来进行一些操作,例如重启框架、停止框架、执行函数等。
|
||||
|
||||
!!! warning 注意
|
||||
::: warning 注意
|
||||
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
|
||||
|
||||
:::
|
||||
|
||||
### reload
|
||||
|
||||
@@ -174,4 +177,3 @@ vendor/bin/start server --log-theme={主题名}
|
||||
vendor/bin/start server --log-theme=white-term # 如果用的是白色终端,这个主题更友好
|
||||
vendor/bin/start server --log-theme=no-color # 如果不想让 log 带有任何颜色,使用无色主题
|
||||
```
|
||||
|
||||
@@ -40,31 +40,33 @@ public function hello() {
|
||||
|
||||
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()`,`@OnCloseEvent()`
|
||||
|
||||
!!! tip "提示"
|
||||
::: tip 提示
|
||||
|
||||
值得注意的是,由于机器人客户端和炸毛框架的连接是通过 WebSocket 进行的,而 WebSocket 是长连接,所以同一个机器人一次连接下收发消息所用的连接是同一个,所以 Fd 也是相同的。同理,炸毛框架的内部来区分多个机器人也是通过这一 Fd 进行判定的。
|
||||
值得注意的是,由于机器人客户端和炸毛框架的连接是通过 WebSocket 进行的,而 WebSocket 是长连接,所以同一个机器人一次连接下收发消息所用的连接是同一个,所以 Fd 也是相同的。同理,炸毛框架的内部来区分多个机器人也是通过这一 Fd 进行判定的。
|
||||
|
||||
=== "代码"
|
||||
:::
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("测试fd")
|
||||
*/
|
||||
public function testfd() {
|
||||
ctx()->reply("当前机器人连接的fd是:".ctx()->getFd()",机器人QQ是:".ctx()->getRobotId());
|
||||
}
|
||||
```
|
||||
代码
|
||||
|
||||
=== "效果"
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("测试fd")
|
||||
*/
|
||||
public function testfd() {
|
||||
ctx()->reply("当前机器人连接的fd是:".ctx()->getFd().",机器人QQ是:".ctx()->getRobotId());
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我们和连接55555的机器人的私聊
|
||||
) 测试fd
|
||||
( 当前机器人连接的fd是:1,机器人QQ是:55555
|
||||
^ 假设切到了另一个机器人(66666)的私聊
|
||||
) 测试fd
|
||||
( 当前机器人连接的fd是:2,机器人QQ是:66666
|
||||
</chat-box>
|
||||
效果
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'假设我们和连接55555的机器人的私聊'},
|
||||
{type:0,content:'测试fd'},
|
||||
{type:1,content:'当前机器人连接的fd是:1,机器人QQ是:55555'},
|
||||
{type:2,content:'假设切到了另一个机器人(66666)的私聊'},
|
||||
{type:0,content:'测试fd'},
|
||||
{type:1,content:'当前机器人连接的fd是:2,机器人QQ是:66666'},
|
||||
]"></chat-box>
|
||||
|
||||
## getData() - 获取事件完整数据
|
||||
|
||||
@@ -82,11 +84,11 @@ public function onMessage() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 假设我是QQ为123456的用户,私聊发消息
|
||||
) 哈咯!!
|
||||
( 消息类型是:private
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'假设我是QQ为123456的用户,私聊发消息'},
|
||||
{type:0,content:'哈咯!!'},
|
||||
{type:1,content:'消息类型是:private'},
|
||||
]"></chat-box>
|
||||
|
||||
## getRequest() - HTTP 请求对象
|
||||
|
||||
@@ -124,7 +126,7 @@ public function ping() {
|
||||
|
||||
## getRobot() - 获取机器人 API 对象
|
||||
|
||||
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](robot-api.md)。
|
||||
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](../bot/robot-api.md)。
|
||||
|
||||
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()`,`@CQMessage()` 等。
|
||||
|
||||
@@ -132,10 +134,10 @@ public function ping() {
|
||||
ctx()->getRobot()->sendPrivateMsg(123456, "发送私聊消息");
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 正在和机器人聊天
|
||||
( 发送私聊消息
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'正在和机器人聊天'},
|
||||
{type:1,content:'发送私聊消息'},
|
||||
]"></chat-box>
|
||||
|
||||
## getMessage() - 获取消息
|
||||
|
||||
@@ -143,24 +145,26 @@ ctx()->getRobot()->sendPrivateMsg(123456, "发送私聊消息");
|
||||
|
||||
可以使用的事件:`@CQCommand()`,`@CQMessage`,`@CQBefore("message")`,`@CQAfter("message")`
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
/**
|
||||
* @CQMessage(group_id=33333)
|
||||
*/
|
||||
public function groupRepeat() {
|
||||
ctx()->reply(ctx()->getMessage());
|
||||
}
|
||||
```
|
||||
代码
|
||||
|
||||
=== "效果"
|
||||
<chat-box>
|
||||
^ 现在在群33333内,机器人已经成了复读机
|
||||
) 来世还做复读机!!!
|
||||
( 来世还做复读机!!!
|
||||
) 你不许复读!
|
||||
( 你不许复读!
|
||||
</chat-box>
|
||||
```php
|
||||
/**
|
||||
* @CQMessage(group_id=33333)
|
||||
*/
|
||||
public function groupRepeat() {
|
||||
ctx()->reply(ctx()->getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
效果
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'现在在群33333内,机器人已经成了复读机'},
|
||||
{type:0,content:'来世还做复读机!!!'},
|
||||
{type:1,content:'来世还做复读机!!!'},
|
||||
{type:0,content:'你不许复读!'},
|
||||
{type:1,content:'你不许复读!'},
|
||||
]"></chat-box>
|
||||
|
||||
## getUserId() - 获取用户 QQ 号
|
||||
|
||||
@@ -275,16 +279,16 @@ function yourName(){
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 自我介绍
|
||||
( 你叫啥名字呀?
|
||||
) jerry
|
||||
( 好的,可爱的机器人记住你叫 jerry 啦!以后多聊天哦!
|
||||
) 自我介绍
|
||||
( 你叫啥名字呀?
|
||||
^ 10分钟没理机器人
|
||||
( 你都10分钟不理我了,嘤嘤嘤
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'自我介绍'},
|
||||
{type:1,content:'你叫啥名字呀?'},
|
||||
{type:0,content:'jerry'},
|
||||
{type:1,content:'好的,可爱的机器人记住你叫 jerry 啦!以后多聊天哦!'},
|
||||
{type:0,content:'自我介绍'},
|
||||
{type:1,content:'你叫啥名字呀?'},
|
||||
{type:2,content:'10分钟没理机器人'},
|
||||
{type:1,content:'你都10分钟不理我了,嘤嘤嘤'},
|
||||
]"></chat-box>
|
||||
|
||||
## getArgs() - 自动获取参数
|
||||
|
||||
@@ -316,12 +320,12 @@ public function argTest1() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) test
|
||||
( 请输入你要传入的参数内容
|
||||
) test2
|
||||
( 参数内容:test2
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'test'},
|
||||
{type:1,content:'请输入你要传入的参数内容'},
|
||||
{type:0,content:'test2'},
|
||||
{type:1,content:'参数内容:test2'},
|
||||
]"></chat-box>
|
||||
|
||||
`getArgs()` 也有三层封装,在使用过程中避免麻烦的话,推荐使用下面这几种 `get*Arg()` 方式。
|
||||
|
||||
@@ -341,14 +345,14 @@ public function argTest1() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) test abc def argtest
|
||||
( 参数内容:abc def argtest
|
||||
) test
|
||||
( 请输入你要传入的参数内容
|
||||
) abc def
|
||||
( 参数内容:abc def
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'test abc def argtest'},
|
||||
{type:1,content:'参数内容:abc def argtest'},
|
||||
{type:0,content:'test'},
|
||||
{type:1,content:'请输入你要传入的参数内容'},
|
||||
{type:0,content:'abc def'},
|
||||
{type:1,content:'参数内容:abc def'},
|
||||
]"></chat-box>
|
||||
|
||||
## getNextArg()
|
||||
|
||||
@@ -364,14 +368,14 @@ public function argTest1() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) test abc def argtest
|
||||
( 参数内容:abc
|
||||
) test
|
||||
( 请输入你要传入的参数内容
|
||||
) abc
|
||||
( 参数内容:abc
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'test abc def argtest'},
|
||||
{type:1,content:'参数内容:abc'},
|
||||
{type:0,content:'test'},
|
||||
{type:1,content:'请输入你要传入的参数内容'},
|
||||
{type:0,content:'abc'},
|
||||
{type:1,content:'参数内容:abc'},
|
||||
]"></chat-box>
|
||||
|
||||
## getNumArg()
|
||||
|
||||
@@ -389,14 +393,14 @@ public function argTest1() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) test abc 334 argtest
|
||||
( 数字参数内容:334
|
||||
) test abc
|
||||
( 请输入你要传入的数字内容
|
||||
) 998
|
||||
( 参数内容:998
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'test abc 334 argtest'},
|
||||
{type:1,content:'数字参数内容:334'},
|
||||
{type:0,content:'test abc'},
|
||||
{type:1,content:'请输入你要传入的参数内容'},
|
||||
{type:0,content:'998'},
|
||||
{type:1,content:'参数内容:998'},
|
||||
]"></chat-box>
|
||||
|
||||
## copy()
|
||||
|
||||
@@ -418,7 +422,7 @@ public function argTest1() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) test abc 334 argtest
|
||||
( 参数内容:abc, 334, argtest
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'test abc 334 argtest'},
|
||||
{type:1,content:'参数内容:abc, 334, argtest'},
|
||||
]"></chat-box>
|
||||
@@ -60,4 +60,3 @@ for($i = 0; $i < 1000; ++$i) {
|
||||
`$name` 为字符串,是你要用的协程池的名称。
|
||||
|
||||
`$size` 为大小,最大不可超过 Swoole 配置文件中指定的最大协程数量。
|
||||
|
||||
104
docs/component/common/event-tracer.md
Normal 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() - 启用事件跟踪器
|
||||
|
||||
还没写完,不着急。
|
||||
@@ -4,40 +4,44 @@
|
||||
|
||||
## getClassPath()
|
||||
|
||||
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L24)
|
||||
|
||||
根据加载的用户编写的代码类名来获取类所在的文件路径。
|
||||
|
||||
=== "src/Module/Example/Hello.php"
|
||||
**src/Module/Example/Hello.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
class Hello { ... }
|
||||
```
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
class Hello { ... }
|
||||
```
|
||||
|
||||
=== "src/Module/Example/Start.php"
|
||||
**src/Module/Example/Start.php**
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
class Start {
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
Console::info("Path: ".getClassPath(Hello::class));
|
||||
}
|
||||
}
|
||||
```
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
use ZM\Annotation\Swoole\OnStart;
|
||||
class Start {
|
||||
/**
|
||||
* @OnStart()
|
||||
*/
|
||||
public function onStart() {
|
||||
Console::info("Path: ".getClassPath(Hello::class));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "输出结果"
|
||||
**输出结果**
|
||||
|
||||
```
|
||||
[11:12:02] [I] [#0] Path: /mnt/d/project/zhamao-framework/src/Module/Example/Hello.ph
|
||||
```
|
||||
```
|
||||
[11:12:02] [I] [#0] Path: /mnt/d/project/zhamao-framework/src/Module/Example/Hello.ph
|
||||
```
|
||||
|
||||
## 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
|
||||
@@ -134,10 +146,6 @@ set_coroutine_params(["data" => [
|
||||
|
||||
别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。
|
||||
|
||||
## zm_debug()
|
||||
|
||||
同 `Console::debug($msg)`。
|
||||
|
||||
## zm_sleep()
|
||||
|
||||
协程版 `sleep()` 函数。
|
||||
@@ -255,4 +263,61 @@ bot()->sendPrivateMsg(123456, "你好啊!!");
|
||||
|
||||
定义:`getAllFdByConnectType(string $type = 'default'): array`
|
||||
|
||||
当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
|
||||
当 `$type` 为 `qq` 时,则返回所有 OneBot 机器人接入的 WebSocket 连接号。
|
||||
|
||||
## zm_dump()
|
||||
|
||||
更漂亮地输出变量值,可替代 `var_dump()`。
|
||||
|
||||
```php
|
||||
class Pass {
|
||||
public $foo = 123;
|
||||
public $bar = ["a", "b"];
|
||||
}
|
||||
$pass = new Pass();
|
||||
$pass->obj = true;
|
||||
zm_dump($pass);
|
||||
```
|
||||
|
||||

|
||||
|
||||
## zm_config()
|
||||
|
||||
> v2.4.0 起可用。
|
||||
|
||||
同 `ZMConfig::get()`。
|
||||
|
||||
定义:`zm_config($name, $key = null)`。
|
||||
|
||||
有关 ZMConfig 模块的说明,见 [指南 - 基本配置](/guide/basic-config)。
|
||||
|
||||
```php
|
||||
zm_config("global"); //等同于 ZMConfig::get("global");
|
||||
zm_config("global", "swoole"); //等同于 ZMConfig::get("global", "swoole");
|
||||
```
|
||||
|
||||
## zm_info()
|
||||
|
||||
> v2.4.0 起可用。(下面的 log 类也一样)
|
||||
|
||||
同 `Console::info($msg)`。
|
||||
|
||||
## zm_debug()
|
||||
|
||||
同 `Console::debug($msg)`。
|
||||
|
||||
## zm_warning()
|
||||
|
||||
同 `Console::warning($msg)`。
|
||||
|
||||
## zm_success()
|
||||
|
||||
同 `Console::success($msg)`。
|
||||
|
||||
## zm_error()
|
||||
|
||||
同 `Console::error($msg)`。
|
||||
|
||||
## zm_verbose()
|
||||
|
||||
同 `Console::verbose($msg)`。
|
||||
44
docs/component/common/remote-terminal.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# 远程终端
|
||||
|
||||
框架在 2.3 版本时删除了本地终端(就是框架启动后可以在终端输入一些参数),因为框架的多进程模式会导致终端输入错乱,所以暂时取消掉了。
|
||||
|
||||
而远程终端应运而生,为的是弥补这一功能。与之前不同的是,远程终端使用 nc 连接,无需任何其他组件和客户端,而且功能更丰富,支持自定义命令。
|
||||
|
||||
## 启用
|
||||
|
||||
有两种开启方式:
|
||||
|
||||
- 永久开启:全局配置文件中找到 `remote_terminal` 的 `status`,改为 true,启动框架即可。
|
||||
- 临时开启:启动框架时加上参数 `--remote-terminal`。例如:`vendor/bin/start server --remote-terminal`。
|
||||
|
||||
## 配置
|
||||
|
||||
在一般情况下,框架为了安全,直接按照默认配置,会监听 `127.0.0.1:20002` 端口,不可以远程访问,只能使用本机的 nc 连接,效果如下:
|
||||
|
||||
本地主机:
|
||||
|
||||

|
||||
|
||||
从别的主机:
|
||||
|
||||

|
||||
|
||||
如果将 `host` 改为 `0.0.0.0` 或对应监听地址,即可指向性访问。
|
||||
|
||||
但是,如果你又想远程连接,又想保证安全,那么可以设置一个 token 参数,来保证连接时需要输入 token 才能使用远程终端。
|
||||
假设我们的 token 是 `iAMTokEn`:
|
||||
|
||||

|
||||
|
||||
## 使用
|
||||
|
||||
默认情况下,使用 `nc` 命令即可。
|
||||
|
||||
```bash
|
||||
nc <your-host> <your-port> -vvv
|
||||
# nc 127.0.0.1 20002 -vvv
|
||||
```
|
||||
|
||||
输入 help 即可查看内置的常用指令:
|
||||
|
||||

|
||||
@@ -1,4 +1,4 @@
|
||||
# 单例类(SingletonTrait)
|
||||
# 单例类 - SingletonTrait
|
||||
|
||||
单例类,顾名思义,就是让用户声明的类拥有单例的特性,而这一组件引入的方式也最直接。它是一个 PHP 的 `trait`。
|
||||
|
||||
@@ -40,4 +40,4 @@ Foo::getInstance()->test = 5;
|
||||
var_dump(Foo::getInstance()->test);
|
||||
```
|
||||
|
||||
只需要在类中使用:`use \ZM\Utils\SingletonTrait;` 一句话即可。
|
||||
只需要在类中使用:`use \ZM\Utils\SingletonTrait;` 一句话即可。
|
||||
25
docs/component/common/task-worker.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# TaskManager 工作进程管理
|
||||
|
||||
此类管理的是 TaskWorker 相关工作。有关使用 TaskWorker 的教程,见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)
|
||||
|
||||
类定义:`\ZM\Utils\Manager\TaskManager`
|
||||
|
||||
使用 TaskWorker 需要先在 `global.php` 配置文件中开启!
|
||||
|
||||
## 方法
|
||||
|
||||
### runTask()
|
||||
|
||||
在 TaskWorker 运行任务。
|
||||
|
||||
定义:`runTask($task_name, $timeout = -1, ...$params)`
|
||||
|
||||
参数 `$task_name`:对应 `@OnTask` 注解绑定的任务函数。
|
||||
|
||||
参数 `$timeout`:等待任务函数最长运行的时间(秒),如果超过此时间将返回 false。
|
||||
|
||||
参数 `剩余`:将变量传入 TaskWorker 进程,除 Closure,资源类型外,可序列化的变量均可。
|
||||
|
||||
```php
|
||||
TaskManager::runTask("heavy_task", 100, "param1", "param2");
|
||||
```
|
||||
66
docs/component/common/zmutil.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# ZMUtil 杂项工具类
|
||||
|
||||
调用前先 use:`use ZM\Utils\ZMUtil;`
|
||||
|
||||
## ZMUtil::stop()
|
||||
|
||||
停止框架运行。
|
||||
|
||||
## ZMUtil::reload()
|
||||
|
||||
重载框架,这会断开所有到框架的连接和重载所有 `src/` 目录下的用户源码并重新加载所有 Worker 进程。
|
||||
|
||||
## ZMUtil::getModInstance()
|
||||
|
||||
根据类名称拿到此类的单例(前提是目标的类的构造函数为空)。
|
||||
|
||||
```php
|
||||
class ASD{
|
||||
public $test = 0;
|
||||
}
|
||||
ZMUtil::getModInstance(ASD::class)->test = 5;
|
||||
```
|
||||
|
||||
## ZMUtil::getReloadableFiles()
|
||||
|
||||
返回可通过热重启(reload)来重新加载的 php 文件列表。
|
||||
|
||||
以下是示例模块下的例子(直接拉取最新的框架源码并运行框架后获取的)。
|
||||
|
||||
```php
|
||||
array:31 [
|
||||
94 => "src/ZM/Context/Context.php"
|
||||
95 => "src/ZM/Context/ContextInterface.php"
|
||||
96 => "src/ZM/Annotation/AnnotationParser.php"
|
||||
97 => "src/Custom/Annotation/Example.php"
|
||||
98 => "src/ZM/Annotation/Interfaces/CustomAnnotation.php"
|
||||
99 => "src/Module/Example/Hello.php"
|
||||
100 => "src/ZM/Annotation/Swoole/OnStart.php"
|
||||
101 => "src/ZM/Annotation/CQ/CQCommand.php"
|
||||
102 => "src/ZM/Annotation/Interfaces/Level.php"
|
||||
103 => "src/ZM/Annotation/Command/TerminalCommand.php"
|
||||
104 => "src/ZM/Annotation/Http/RequestMapping.php"
|
||||
105 => "src/ZM/Annotation/Http/RequestMethod.php"
|
||||
106 => "src/ZM/Annotation/Http/Middleware.php"
|
||||
107 => "src/ZM/Annotation/Interfaces/ErgodicAnnotation.php"
|
||||
108 => "src/ZM/Annotation/Swoole/OnOpenEvent.php"
|
||||
109 => "src/ZM/Annotation/Swoole/OnSwooleEventBase.php"
|
||||
110 => "src/ZM/Annotation/Interfaces/Rule.php"
|
||||
111 => "src/ZM/Annotation/Swoole/OnCloseEvent.php"
|
||||
112 => "src/ZM/Annotation/Swoole/OnRequestEvent.php"
|
||||
113 => "src/ZM/Http/RouteManager.php"
|
||||
114 => "vendor/symfony/routing/RouteCollection.php"
|
||||
115 => "vendor/symfony/routing/Route.php"
|
||||
116 => "src/Module/Middleware/TimerMiddleware.php"
|
||||
117 => "src/ZM/Http/MiddlewareInterface.php"
|
||||
118 => "src/ZM/Annotation/Http/MiddlewareClass.php"
|
||||
119 => "src/ZM/Annotation/Http/HandleBefore.php"
|
||||
120 => "src/ZM/Annotation/Http/HandleAfter.php"
|
||||
121 => "src/ZM/Annotation/Http/HandleException.php"
|
||||
122 => "src/ZM/Event/EventManager.php"
|
||||
123 => "src/ZM/Annotation/Swoole/OnSwooleEvent.php"
|
||||
124 => "src/ZM/Event/EventDispatcher.php"
|
||||
]
|
||||
```
|
||||
|
||||
> 为什么不能重载所有文件?因为框架是多进程模型,而重载相当于只重新启动了一次 Worker 进程,Manager 和 Master 进程未重启,所以被 Manager、Master 进程已经加载的 PHP 文件无法使用 reload 命令重新加载。详见 [进阶 - 进程间隔离](/advanced/multi-process/#_5)。
|
||||
@@ -1,37 +0,0 @@
|
||||
# 存储管理(文件)
|
||||
|
||||
DataProvider 是框架内提供的一个简易的文件管理类。
|
||||
|
||||
定义:`\ZM\Utils\DataProvider`
|
||||
|
||||
## DataProvider::getWorkingDir()
|
||||
|
||||
同 `working_dir()`。
|
||||
|
||||
## DataProvider::getFrameworkLink()
|
||||
|
||||
同 `ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
|
||||
|
||||
## DataProvider::getDataFolder()
|
||||
|
||||
获取配置项 `zm_data` 指定的目录。
|
||||
|
||||
## DataProvider::saveToJson()
|
||||
|
||||
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
|
||||
|
||||
定义:`saveToJson($filename, $file_array)`
|
||||
|
||||
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
|
||||
|
||||
这里只支持二级目录,不支持更多级的子目录。
|
||||
|
||||
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
|
||||
|
||||
## DataProvider::loadFromJson()
|
||||
|
||||
从 json 文件加载内容至变量。
|
||||
|
||||
定义:`loadFromJson($filename)`
|
||||
|
||||
文件名同上 `saveToJson()` 的定义,解析后的返回值为原先的内容或 `null`(如果文件不存在或 json 解析失败)。
|
||||
56
docs/component/http/route-manager.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# HTTP 路由管理
|
||||
|
||||
HTTP 路由管理器用作管理炸毛框架内 `@RequestMapping` 和静态目录的路由操作的,可在运行过程中编写添加路由。
|
||||
|
||||
类定义:`\ZM\Http\RouteManager`
|
||||
|
||||
> 2.3.0 版本起可用。
|
||||
|
||||
::: warning 注意
|
||||
|
||||
因为炸毛框架的路由实现是不基于跨进程的共享内存的,所以每次使用这里面的工具函数都需要单独在所有 Worker 进程中执行一次,最好的办法就是在启动框架时执行(`@OnStart(-1)` 即可,代表此注解事件将在每个工作进程中都被执行一次)。
|
||||
|
||||
:::
|
||||
|
||||
## 方法
|
||||
|
||||
### importRouteByAnnotation()
|
||||
|
||||
通过注解类导入路由。(注:此方法一般为框架内部使用)
|
||||
|
||||
定义:`importRouteByAnnotation(RequestMapping $vss, $method, $class, $methods_annotations)`
|
||||
|
||||
参数 `$vss`:RequestMapping 注解类,类中定义 `route` 和 `request_method` 即可。
|
||||
|
||||
参数 `$method, $class`:执行的目标注解事件函数位置,比如 `$class = \Module\Example\Hello::class`,`$method = 'hitokoto'`。
|
||||
|
||||
参数 `$methods_annotations`:需要绑定的 Controller 注解类数组,一般数组内建议只带有一个 Controller,如 `[$controller]`。
|
||||
|
||||
### addStaticFileRoute()
|
||||
|
||||
添加一个单目录(此目录下无子目录,只有文件)并绑定为一个路由。
|
||||
|
||||
定义:`addStaticFileRoute($route, $path)`
|
||||
|
||||
参数 `$route`:绑定的目标路由,如 `/images/`。
|
||||
|
||||
参数 `$path`:绑定的文件目录位置,如 `/root/zhamao-framework-starter/zm_data/images/`。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @OnStart(-1)
|
||||
*/
|
||||
public function onStart() {
|
||||
RouteManager::addStaticFileRoute("/images/", DataProvider::getDataFolder()."images/");
|
||||
}
|
||||
```
|
||||
|
||||
## 属性
|
||||
|
||||
### RouteManager::$routes
|
||||
|
||||
此为存放路由树的变量,请谨慎操作。
|
||||
|
||||
定义:`\Symfony\Component\Routing\RouteCollection | null`
|
||||
|
||||
炸毛框架使用了 Symfony 框架的 route 组件,有关详情请查阅 [文档](https://symfony.com/doc/current/routing.html)。
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
命名空间:`use ZM\Requests\ZMRequest;`
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
在使用 Swoole 4.6.0 以下(不包含)的版本时,最好使用 Swoole 官方推荐的 Saber 或者 ZMRequest 这个轻量的 HTTP 请求客户端,不要使用 curl_exec,因为在老版本的 Swoole 上对 curl 的协程 Hook 支持不是很完善。
|
||||
在使用 Swoole 4.6.0 以下(不包含)的版本时,最好使用 Swoole 官方推荐的 Saber 或者 ZMRequest 这个轻量的 HTTP 请求客户端,不要使用 curl_exec,因为在老版本的 Swoole 上对 curl 的协程 Hook 支持不是很完善。
|
||||
|
||||
:::
|
||||
|
||||
## ZMRequest::get()
|
||||
|
||||
@@ -262,11 +263,12 @@ $a->onClose(function($client){
|
||||
|
||||
返回值:`true|false`,当为 `true` 时代表握手成功,此时可以在回调里愉快地收发消息了。如果为 `false` 表明握手失败。
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
这里由于是协程转异步,所以不能确定 `upgrade()` 和 `onMessage()` 哪个先会被触发(一般情况下如果服务器不是立刻响应回包信息,总是会先返回 `upgrade()` 的结果。
|
||||
这里由于是协程转异步,所以不能确定 `upgrade()` 和 `onMessage()` 哪个先会被触发(一般情况下如果服务器不是立刻响应回包信息,总是会先返回 `upgrade()` 的结果。
|
||||
|
||||
:::
|
||||
|
||||
## 设置参数
|
||||
|
||||
见:[Swoole - HTTP 客户端](http://wiki.swoole.com/#/coroutine_client/http_client?id=set)
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 框架组件
|
||||
|
||||
这里列举了框架内的你可能会用到的常用组件。
|
||||
264
docs/component/module/module-pack.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 模块打包
|
||||
|
||||
从 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`。
|
||||
|
||||
- 含义:模块的描述。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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)。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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)。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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 模块包内。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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,启动框架即可加载。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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` 文件,下方是实例。
|
||||
|
||||
::: tip 编写实例
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
通过此命令可以查看模块相关的信息,如未打包但已配置的模块信息等。
|
||||
84
docs/component/module/module-unpack.md
Normal 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,则会等待你手动修改完成文件,最后按回车完成修改。默认情况直接回车的话,会跳过此步骤,如果模块要求了修改但跳过修改,安装后可能会有功能缺失等问题。
|
||||
@@ -16,10 +16,11 @@ $config['init_atomics'] = [
|
||||
|
||||
这时我们就成功初始化两个原子计数器,名字分别为 `foo` 和 `bar`。
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
初始化的值必须是不小于 0 的 int32 值!
|
||||
初始化的值必须是不小于 0 的 int32 值!
|
||||
|
||||
:::
|
||||
|
||||
## 使用
|
||||
|
||||
@@ -59,7 +60,8 @@ class Hello {
|
||||
|
||||
设置计数的数字:`ZMAtomic::get("bar")->set(77);`
|
||||
|
||||
!!! note "提示"
|
||||
::: tip 提示
|
||||
|
||||
还有一些不常用的方法,可以看 Swoole 官方的文档,这里就不一一列举了。
|
||||
还有一些不常用的方法,可以看 Swoole 官方的文档,这里就不一一列举了。
|
||||
|
||||
:::
|
||||
108
docs/component/store/data-provider.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# 存储管理(文件)
|
||||
|
||||
DataProvider 是框架内提供的一个简易的文件管理类。
|
||||
|
||||
定义:`\ZM\Utils\DataProvider`
|
||||
|
||||
## DataProvider::getWorkingDir()
|
||||
|
||||
同 `working_dir()`。
|
||||
|
||||
## DataProvider::getSourceRootDir()
|
||||
|
||||
获取用户的源码根目录,除 Phar 模式外与 `getWorkingDir()` 相同。
|
||||
|
||||
## DataProvider::getFrameworkLink()
|
||||
|
||||
同 `ZMConfig::get("global", "http_reverse_link")`,获取反向代理的链接。
|
||||
|
||||
## DataProvider::getDataFolder()
|
||||
|
||||
获取配置项 `zm_data` 指定的目录。
|
||||
|
||||
如果指定参数 `$second`,则返回二级目录地址,如果二级目录不存在则自动创建。
|
||||
|
||||
```php
|
||||
DataProvider::getDataFolder("TestModule"); // 例如返回 /root/zhamao-framework/zm_data/TestModule/
|
||||
```
|
||||
|
||||
## DataProvider::getFrameworkRootDir()
|
||||
|
||||
返回框架本体的根目录。
|
||||
|
||||
## DataProvider::saveToJson()
|
||||
|
||||
将变量内容保存为 json 格式的文件,存储在 `zm_data/config/` 目录下或子目录下。
|
||||
|
||||
定义:`saveToJson($filename, $file_array)`
|
||||
|
||||
`$filename` 是文件名,不需要加后缀,比如你想保存成 `foo/bar.json`,这里写 `foo/bar` 就好。如果不想要二级目录,就直接写 `bar`,不需要加 `.json` 后缀。
|
||||
|
||||
这里只支持二级目录,不支持更多级的子目录。
|
||||
|
||||
`$file_array` 为内容,一般是数组,比如你缓存了一个 API 接口返回的数据,然后直接解析成数组后丢给它就好了。
|
||||
|
||||
## DataProvider::loadFromJson()
|
||||
|
||||
从 json 文件加载内容至变量。
|
||||
|
||||
定义:`loadFromJson($filename)`
|
||||
|
||||
文件名同上 `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`)。但有一点要注意,框架内最好使用**工作目录或者绝对路径**。
|
||||
|
||||
```php
|
||||
// 读取框架工作目录的文件 composer.json 文件
|
||||
$r = file_get_contents(working_dir() . "/composer.json");
|
||||
|
||||
// 写入 Linux 临时目录下的文件
|
||||
file_put_contents("/tmp/test.txt", "hello world");
|
||||
```
|
||||
|
||||
::: warning 注意
|
||||
|
||||
在默认的情况里,框架的根目录均为可写可读的,在读写文件时务必要注意目录的位置和权限。使用 `working_dir()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。
|
||||
|
||||
:::
|
||||
@@ -57,7 +57,9 @@ $config['light_cache'] = [
|
||||
|
||||
`$value` 可存入 `bool`、`string`、`int`、`array` 等可被 `json_encode()` 的变量,闭包函数和对象不可存入。
|
||||
|
||||
`$expire` 是 `int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除。如果为 -1 则什么都不做,如果框架使用了 `stop` 或 Ctrl+C 或意外退出时数据会丢失。如果为 -2,则会将此数据持久化保存,保存在上方配置文件指定的 json 文件中,待关闭后再次启动框架会自动加载回来,不会丢失。
|
||||
`$expire` 是 `int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除(框架中途停止不受影响)。如果为 -1 则什么都不做。框架停止后自动被清除。
|
||||
|
||||
**注意:如果前面使用了 set() ,后面再次使用 set() 会重置 expire 过期时间为 -1(-1 是框架运行时不过期,关闭框架删除的状态),如果只需要更新值,请使用 update()。**
|
||||
|
||||
```php
|
||||
// use ZM\Store\LightCache;
|
||||
@@ -79,14 +81,22 @@ public function storeAfterRemove() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) store
|
||||
( OK!
|
||||
) storeAfterRemove
|
||||
( 内容存在!
|
||||
^ 等待 30 秒
|
||||
( 内容不存在!
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'store'},
|
||||
{type:1,content:'OK!'},
|
||||
{type:0,content:'storeAfterRemove'},
|
||||
{type:1,content:'内容存在!'},
|
||||
{type:2,content:'等待 30 秒'},
|
||||
{type:1,content:'内容不存在!'},
|
||||
]"></chat-box>
|
||||
|
||||
### LightCache::update()
|
||||
|
||||
更新值而不更新状态。如果键值对不存在,则返回 false。
|
||||
|
||||
定义:`LightCache::update(string $key, $value)`
|
||||
|
||||
参数同 `set()`,可参考。
|
||||
|
||||
### LightCache::get()
|
||||
|
||||
@@ -106,12 +116,26 @@ zm_sleep(10);
|
||||
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::getExpireTS("test")); // 返回 1616838502
|
||||
zm_sleep(10);
|
||||
dump(LightCache::getExpireTS("test")); // 返回 null
|
||||
```
|
||||
|
||||
### LightCache::getMemoryUsage()
|
||||
|
||||
获取轻量缓存使用的总空间大小(字节)
|
||||
|
||||
```php
|
||||
LightCache::getMemoryUsage());
|
||||
LightCache::getMemoryUsage();
|
||||
```
|
||||
|
||||
轻量缓存的内存手工计算方式:(Table 结构体长度` + `KEY 长度 64 字节 + `$size`) * (1 + `$conflict_proportion`) * 列尺寸。
|
||||
@@ -157,25 +181,34 @@ dump(LightCache::getAll());
|
||||
*/
|
||||
```
|
||||
|
||||
### LightCache::savePersistence()
|
||||
### LightCache::addPersistence()
|
||||
|
||||
立刻保存所有被标记为持久化的缓存项到磁盘。
|
||||
添加持久化存储的键。
|
||||
|
||||
!!! note "提示"
|
||||
用法:`LightCache::addPersistence($key)`。
|
||||
|
||||
在一般情况下,框架定时执行此方法来保存,在停止框架、reload 框架和 Ctrl+C 停止框架的时候,均会执行保存。
|
||||
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为 -2 了。(2.4.2 起可用此方法)。
|
||||
|
||||
详见下方 **持久化**。
|
||||
|
||||
### LightCache::removePersistence()
|
||||
|
||||
删除持久化的键。
|
||||
|
||||
用法:`LightCache::removePersistence($key)`。
|
||||
|
||||
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为非 -2 了。(2.4.2 起可用此方法)。
|
||||
|
||||
### 持久化
|
||||
|
||||
将 `set()` 的 expire 设置为 -2 即可。
|
||||
使用 `LightCache::addPersistence($key)` 添加对应需要持久化的键名即可。
|
||||
|
||||
```php
|
||||
/**
|
||||
* @CQCommand("store")
|
||||
* @OnStart()
|
||||
*/
|
||||
public function store() {
|
||||
LightCache::set("msg_time", time(), -2);
|
||||
return "OK!";
|
||||
public function onStart() {
|
||||
LightCache::addPersistence("msg_time");
|
||||
}
|
||||
/**
|
||||
* @CQCommand("getStore")
|
||||
@@ -185,14 +218,14 @@ public function getStore() {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
^ 我在 2021-01-05 15:21:00 发送这条消息
|
||||
) store
|
||||
( OK!
|
||||
^ 这时我用 Ctrl+C 停止框架,过一会儿再启动
|
||||
) getStore
|
||||
( 存储时间:2021-01-05 15:21:00
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:2,content:'我在 2021-01-05 15:21:00 发送这条消息'},
|
||||
{type:0,content:'getStore'},
|
||||
{type:1,content:'2021-01-05 15:20:00'},
|
||||
{type:2,content:'这时我用 Ctrl+C 停止框架,过一会儿再启动'},
|
||||
{type:0,content:'getStore'},
|
||||
{type:1,content:'存储时间:2021-01-05 15:20:00'},
|
||||
]"></chat-box>
|
||||
|
||||
### 数据加锁
|
||||
|
||||
@@ -217,10 +250,11 @@ public function test() {
|
||||
|
||||
在运行完测试后,通过 `LightCache::get("web_count")`,获取到的数你会发现不是 200000。怎么回事呢?请自行翻阅多进程开发相关的书籍哦!(或者简单理解为,有一些情况下,进程 1 执行到了 `if-else` 语句,另一个进程也执行到了这里,两次在代码层面加的数是相同的,则虽然请求了两次,但是后执行 set 的那个进程又覆盖了前一个进程执行的值,导致最终结果加了 1 而不是 2)
|
||||
|
||||
!!! note "提示"
|
||||
::: tip 提示
|
||||
|
||||
同样的场景,使用 ZMAtomic 就不需要使用锁了。Atomic 是一句话:`add(1)` 立即加值的。而 LightCache 需要加锁的情况一般都是 `get->改值->set` 这样的代码。
|
||||
同样的场景,使用 ZMAtomic 就不需要使用锁了。Atomic 是一句话:`add(1)` 立即加值的。而 LightCache 需要加锁的情况一般都是 `get->改值->set` 这样的代码。
|
||||
|
||||
:::
|
||||
|
||||
解决这一问题,就需要用到锁。这种情况下,我们首先考虑的是自旋锁,框架也因此内置了一个方便使用的自旋锁组件。详见下一章:自旋锁。
|
||||
|
||||
@@ -238,13 +272,13 @@ public function test() {
|
||||
|
||||
### WorkerCache 跨进程大缓存
|
||||
|
||||
WorkerCache 和 LightCache 几乎完全不同,WorkerCache 存储的方式说白了就是 PHP 的静态变量,不过框架支持使用封装好的进程间通信进行跨进程读取。但由于需要设置一个存储变量的进程,所以配置文件必须先指定要将数据存到哪个 Worker/TaskWorker 进程中。关于框架内多进程的说明,请见 [进阶 - 多进程 Hack](/advanced/multi-process/)。
|
||||
WorkerCache 和 LightCache 几乎完全不同,WorkerCache 存储的方式说白了就是 PHP 的静态变量,不过框架支持使用封装好的进程间通信进行跨进程读取。但由于需要设置一个存储变量的进程,所以配置文件必须先指定要将数据存到哪个 Worker/TaskWorker 进程中。关于框架内多进程的说明,请见 [进阶 - 多进程 Hack](/advanced/multi-process)。
|
||||
|
||||
定义:`ZM\Store\WorkerCache`。
|
||||
|
||||
#### 配置
|
||||
|
||||
见 [基本配置](/guide/basic-config/)。
|
||||
见 [基本配置](/guide/basic-config)。
|
||||
|
||||
#### WorkerCache::get()
|
||||
|
||||
@@ -309,12 +343,11 @@ class Hello {
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) set_store hello world
|
||||
( 成功!
|
||||
) get_store hello
|
||||
( world
|
||||
) get_store foo
|
||||
( 内容不存在!
|
||||
</chat-box>
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'set_store hello world'},
|
||||
{type:1,content:'成功!'},
|
||||
{type:0,content:'get_store hello'},
|
||||
{type:1,content:'world'},
|
||||
{type:0,content:'get_store foo'},
|
||||
{type:1,content:'内容不存在!'},
|
||||
]"></chat-box>
|
||||
@@ -1,4 +1,10 @@
|
||||
# MySQL 数据库
|
||||
# MySQL 数据库(旧版组件)
|
||||
|
||||
::: warning 注意
|
||||
|
||||
此 MySQL 组件为旧版 MySQL 查询器组件,为了统一和提升对未来独立组件的兼容性,现转变为使用 `doctrine/dbal` 和 `doctrine/orm` 库来实现查询器,请转到 [MySQL 查询器]()。
|
||||
|
||||
:::
|
||||
|
||||
## 配置
|
||||
|
||||
@@ -74,8 +80,6 @@ DB::table("admin")->where("name", "fake_admin")->count();
|
||||
//SELECT count(*) FROM admin WHERE name = 'fake_admin'
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 直接执行 SQL
|
||||
|
||||
> 在查询器外执行的 SQL 语句都不会被缓存,都是一定会请求数据库的。
|
||||
@@ -94,4 +98,4 @@ $r = DB::rawQuery("SELECT * FROM admin WHERE name = ?", ["fake_admin"]);
|
||||
echo $r[0]["password"];
|
||||
```
|
||||
|
||||
> 参数查询已经从根本上杜绝了 SQL 注入的问题。
|
||||
> 参数查询已经从根本上杜绝了 SQL 注入的问题。
|
||||
3
docs/component/store/mysql-statement.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# MySQLStatement
|
||||
|
||||
你好啊,这里是 Statement。TODO
|
||||
208
docs/component/store/mysql.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 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 |
|
||||
|
||||
## 配置
|
||||
|
||||
炸毛框架的数据库组件支持原生 SQL、查询构造器,去掉了复杂的对象模型关联,同时默认为数据库连接池,使开发变得简单。
|
||||
|
||||
数据库的配置位于 `config/global.php` 文件的 `mysql_config` 段,见 [全局配置](../../../../guide/basic-config#mysql_config)。
|
||||
|
||||
如果 `mysql_config.host` 字段为空,则不创建数据库连接池,填写后将创建,且默认保持长连接。
|
||||
|
||||
## 执行 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)
|
||||
```
|
||||
@@ -59,4 +59,4 @@ ZMRedis::call(function($redis) {
|
||||
|
||||
选一个喜欢的就好。硬要是说区别的话,对象模式是在 PHP 自动回收这个 `ZMRedis` 对象时会归还连接,也可以通过手动 `unset($obj)` 进行回收,否则就会执行到函数结尾自动回收。切记不可将 `$obj` 对象持久化存到静态或全局变量等。
|
||||
|
||||
回调模式看似是回调,但是是同步执行的,不会发生顺序错乱。也就是说到了 `ZMRedis::call()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。
|
||||
回调模式看似是回调,但是是同步执行的,不会发生顺序错乱。也就是说到了 `ZMRedis::call()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
|
||||
|
||||
::: tip 提示
|
||||
|
||||
框架单进程运行的模式下不需要任何自旋锁。
|
||||
|
||||
:::
|
||||
|
||||
## 配置
|
||||
|
||||
自旋锁使用无需配置,和 LightCache 同源。
|
||||
@@ -57,9 +63,11 @@ public function test() {
|
||||
|
||||
原理剖析:在 LightCache 获取前,先对此内容上锁,这时如果其他进程有同时也在执行这个代码的时候,就会在 `SpinLock::lock()` 这行代码处原地等待,防止继续执行。等前面的那个进程执行到 `SpinLock::unlock()` 释放锁时,其他进程才可继续执行,从而避免了多个进程并行执行这段代码导致的数据错乱。
|
||||
|
||||
!!! error "警告"
|
||||
::: danger 警告
|
||||
|
||||
使用锁时务必谨慎,如果不按照下面的规则使用自旋锁可能导致 CPU 占用率上升。
|
||||
使用锁时务必谨慎,如果不按照下面的规则使用自旋锁可能导致 CPU 占用率上升。
|
||||
|
||||
:::
|
||||
|
||||
自旋锁使用约定:
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# ZMUtil 杂项工具类
|
||||
|
||||
调用前先 use:`use ZM\Utils\ZMUtil;`
|
||||
|
||||
## ZMUtil::stop()
|
||||
|
||||
停止框架运行。
|
||||
|
||||
## ZMUtil::reload()
|
||||
|
||||
重载框架,这会断开所有到框架的连接和重载所有 `src/` 目录下的用户源码并重新加载所有 Worker 进程。
|
||||
|
||||
## ZMUtil::getModInstance()
|
||||
|
||||
根据类名称拿到此类的单例(前提是目标的类的构造函数为空)。
|
||||
|
||||
```php
|
||||
class ASD{
|
||||
public $test = 0;
|
||||
}
|
||||
ZMUtil::getModInstance(ASD::class)->test = 5;
|
||||
```
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# 自定义注解
|
||||
|
||||
TODO:师傅,莫催,快肝完了!
|
||||
TODO:师傅,莫催,快肝完了!
|
||||
|
||||
@@ -102,18 +102,18 @@ class Test {
|
||||
|
||||
在炸毛框架内部,一个完整的事件流程和中间件的关系如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
对于同一事件的优先级和响应顺序,优先级的关系如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
对于事件内单个事件被调用的单个函数下如果存在多个中间件,中间件模型和事件的关系如下图:
|
||||
|
||||

|
||||

|
||||
|
||||
## 实战例子
|
||||
|
||||
我们假设 CustomEvent 是我们的自定义注解。还没写完,这部分太复杂了,而且举例子也不好举例,这块应该也不用着急更新。
|
||||
|
||||
TODO:待完成
|
||||
TODO:待完成
|
||||
|
||||
@@ -221,6 +221,27 @@
|
||||
| tick_ms | `int`,**必填**,间隔的毫秒数,例如 1 秒间隔为 `1000`,范围大于 0,小于 86400000。 | | |
|
||||
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0,范围是 0~{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
|
||||
|
||||
## OnTask()
|
||||
|
||||
定义一个在工作进程中运行的任务函数。详情见 [进阶 - 使用 TaskWorker 进程处理密集运算](/advanced/task-worker)。
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | ----------------------------- |
|
||||
| 名称 | `@OnTask` |
|
||||
| 触发前提 | 在框架加载后激活 |
|
||||
| 命名空间 | `ZM\Annotation\Swoole\OnTask` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 有,返回 Worker 进程的结果 |
|
||||
|
||||
### 注解参数
|
||||
|
||||
| 参数名称 | 参数范围 | 用途 | 默认 |
|
||||
| --------- | ------------------------------------------------------------ | ------------ | ---- |
|
||||
| task_name | `string`,**必填**,任务函数的名称,不建议重复。 | | |
|
||||
| rule | 设置触发前提,PHP 代码,返回 bool 值即可,参考 OnRequestEvent | 限定是否执行 | 空 |
|
||||
|
||||
## OnSetup()
|
||||
|
||||
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。
|
||||
@@ -241,6 +262,28 @@
|
||||
|
||||
无。
|
||||
|
||||
## TerminalCommand()
|
||||
|
||||
添加一个远程终端的自定义命令。(2.4.0 版本起可用)
|
||||
|
||||
### 属性
|
||||
|
||||
| 类型 | 值 |
|
||||
| ---------- | --------------------------------------- |
|
||||
| 名称 | `@TerminalCommand` |
|
||||
| 触发前提 | 连接到远程终端可触发 |
|
||||
| 命名空间 | `ZM\Annotation\Command\TerminalCommand` |
|
||||
| 适用位置 | 方法 |
|
||||
| 返回值处理 | 无 |
|
||||
|
||||
### 注解参数
|
||||
|
||||
| 参数名称 | 参数范围 | 默认 |
|
||||
| ----------- | ------------------------------ | ---- |
|
||||
| command | `string`,**必填**,命令字符串 | |
|
||||
| alias | `string`,可选,命令的别名 | |
|
||||
| description | `string`,要显示的帮助文本 | 空 |
|
||||
|
||||
## 示例1(机器人连接框架后输出信息)
|
||||
|
||||
```php
|
||||
@@ -285,7 +328,7 @@ class Hello {
|
||||
|
||||
## 示例3(接收 WS 客户端发来的数据)
|
||||
|
||||
见 [接入 WebSocket 客户端](/advanced/connect-ws-client/)。
|
||||
见 [接入 WebSocket 客户端](/advanced/connect-ws-client)。
|
||||
|
||||
## 示例4(使用 OnStart 给所有 Worker 进程写入缓存提速)
|
||||
|
||||
@@ -316,8 +359,7 @@ class Hello {
|
||||
},
|
||||
"key": "C",
|
||||
"answer_type": 0
|
||||
},
|
||||
.....
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -368,10 +410,10 @@ class Hello {
|
||||
|
||||
聊天效果:
|
||||
|
||||
<chat-box>
|
||||
) 找题 1
|
||||
( 题目名称:法律与其他社会规范的区别在于( )。\nA. 是调整人们行为的规范\nB. 有约束力\nC. 由国家强制力保证执行\nD. 规定制裁措施\n正确答案:C
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'找题 1'},
|
||||
{type:1,content:'题目名称:法律与其他社会规范的区别在于( )。\nA. 是调整人们行为的规范\nB. 有约束力\nC. 由国家强制力保证执行\nD. 规定制裁措施\n正确答案:C'},
|
||||
]"></chat-box>
|
||||
|
||||
## 示例5(创建每分钟自动执行的爬虫)
|
||||
|
||||
@@ -385,3 +427,7 @@ public function onCrawl() {
|
||||
}
|
||||
```
|
||||
|
||||
## 示例6(创建一个远程终端命令并调试框架)
|
||||
|
||||
> 开个坑,以后填。(__填坑标记__)
|
||||
>
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
## 注解事件概念
|
||||
|
||||
我们知道事件,是一个底层的 event loop 收到消息后调用对应的各类方法的一个模型,比如给机器人发送消息后框架要做的就是指定到一个你定义的函数上,处理你的业务逻辑代码。比如在默认模块中,提供了 **你好** 的回复:**你好啊,我是由炸毛框架构建的机器人!**。这项简单回复的任务就是一个事件的触发到响应的全过程。
|
||||
我们知道事件,是一个底层的 event loop 收到消息后调用对应的各类方法的一个模型,比如给机器人发送消息后框架要做的就是指定到一个你定义的函数上,处理你的业务逻辑代码。比如在默认模块中,提供了 **你好** 的回复:**
|
||||
你好啊,我是由炸毛框架构建的机器人!**。这项简单回复的任务就是一个事件的触发到响应的全过程。
|
||||
|
||||
**注解**(Annotation)又称标注,Java 最早在 2004 年的 JDK 5 中引入的一种注释机制。目前 PHP 官方版本并未提供内置元注解和注解概念,但我们通过 `ReflectionClass` 反射类解析 PHP 代码注释从而实现了自己的一套注解机制。如果你没有写过 Java,并且不了解注解是什么,你可以理解为对 function 或 class 的一个修饰,因为传统的 PHP 代码逻辑我们都知道,不能简单给原先存在的函数贴标签,就比如,你不能在原本的 PHP 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。
|
||||
**注解**(Annotation)又称标注,Java 最早在 2004 年的 JDK 5 中引入的一种注释机制。目前 PHP 官方版本并未提供内置元注解和注解概念,但我们通过 `ReflectionClass` 反射类解析 PHP
|
||||
代码注释从而实现了自己的一套注解机制。如果你没有写过 Java,并且不了解注解是什么,你可以理解为对 function 或 class 的一个修饰,因为传统的 PHP
|
||||
代码逻辑我们都知道,不能简单给原先存在的函数贴标签,就比如,你不能在原本的 PHP 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。
|
||||
|
||||
在常见框架如 Spring,Swoft 等代码结构里面,注解更是其核心的存在。
|
||||
|
||||
在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 Swoole 自身的框架启动事件、WebSocket 连接握手事件、HTTP 请求事件等等,也包括 CQHTTP 发来的事件,如`message`,`notice`,`request` 等。
|
||||
在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 Swoole 自身的框架启动事件、WebSocket 连接握手事件、HTTP 请求事件等等,也包括 CQHTTP 发来的事件,如`message`,`notice`
|
||||
,`request` 等。
|
||||
|
||||
## 如何使用注解
|
||||
|
||||
@@ -29,9 +33,11 @@ class Hello {
|
||||
}
|
||||
```
|
||||
|
||||
其中 `@CQCommand()` 就是一个基本的注解应用。注意需引入相关注解(Annotation)类,**且必须** 以 `/**` 开始并以 `*/` 结束,否则会导致无法解析!上方 `@return` 为 IDE 自动生成的 PHPDoc,不需要管。
|
||||
其中 `@CQCommand()` 就是一个基本的注解应用。注意需引入相关注解(Annotation)类,**且必须** 以 `/**` 开始并以 `*/` 结束,否则会导致无法解析!上方 `@return` 为 IDE 自动生成的
|
||||
PHPDoc,不需要管。
|
||||
|
||||
有什么用?大有妙用!这个例子内注解类的用途是收到 QQ 消息后如果消息第一个词匹配到 `你好` 的话,框架就会自动处理,最终执行调用此 `hello()` 方法。注意 `CQCommand` 和其他任何后面讲到的注解类一样,需先 `use ZM\Annotation\` 下的对应注解类,否则也不能正常使用。
|
||||
有什么用?大有妙用!这个例子内注解类的用途是收到 QQ 消息后如果消息第一个词匹配到 `你好` 的话,框架就会自动处理,最终执行调用此 `hello()` 方法。注意 `CQCommand`
|
||||
和其他任何后面讲到的注解类一样,需先 `use ZM\Annotation\` 下的对应注解类,否则也不能正常使用。
|
||||
|
||||
### 基本语法
|
||||
|
||||
@@ -47,11 +53,27 @@ class Hello {
|
||||
|
||||
对于没有参数的注解类,`@参数名()` 直接使用即可。
|
||||
|
||||
### PHP8 原生 Attribute 使用
|
||||
|
||||
在 2.7.0 版本更新后,框架已经完全支持使用原生 Attribute 替代此前的 Annotation。如果你使用 PHP8.0 或以上版本,即可使用 Attribute 以取得最佳的原生编程体验。两者的参数和效果完全一致。
|
||||
|
||||
以 `@CQCommand` 为例,在 PHP8 中,你可以使用以下代码达成相同的效果。
|
||||
|
||||
```php
|
||||
#[CQCommand('帮助', alias=['帮助列表', '菜单'])]
|
||||
public function help() {
|
||||
```
|
||||
|
||||
由于两者几乎完全一致,文档将不会加以区分,统称为注解。
|
||||
|
||||
> 关于原生注解的更多信息,请查阅[官方文档](https://www.php.net/manual/zh/language.attributes.php)。
|
||||
|
||||
## 注解和事件的关系
|
||||
|
||||
在炸毛框架里,注解常常被当作事件分发的一个重要角色,但注解本身又不是事件,更恰当的说,是注解代表了事件。
|
||||
|
||||
机器人开发过程中常见的 `@CQCommand`,或者是 HTTP 服务器路由绑定 `@RequestMapping` 都是相当于由对应注解代表了事件,而 `@Middleware`,`@Closed` 等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
|
||||
机器人开发过程中常见的 `@CQCommand`,或者是 HTTP 服务器路由绑定 `@RequestMapping` 都是相当于由对应注解代表了事件,而 `@Middleware`,`@Closed`
|
||||
等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
|
||||
|
||||
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
|
||||
|
||||
@@ -65,6 +87,3 @@ class Hello {
|
||||
EventDispatcher::interrupt();
|
||||
EventDispatcher::interrupt($data); // 也可以带返回值,自定义注解事件时有用。
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
在炸毛框架中,中间件最直白的意思就是注解事件执行前、执行后、执行过程中可进行插入代码但不破坏原有代码。
|
||||
|
||||
```伪代码
|
||||
```
|
||||
@中间件1
|
||||
@带条件的注解1
|
||||
function 我的方法() {
|
||||
@@ -165,3 +165,30 @@ 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,在框架启动时抛出致命异常。
|
||||
|
||||
@@ -4,9 +4,11 @@ QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理
|
||||
|
||||
为了便于开发,这里的注解类对应 CQHTTP 插件返回的 `post_type` 类型,对号入座即可。
|
||||
|
||||
!!! tip "提示"
|
||||
::: tip 提示
|
||||
|
||||
在使用注解绑定事件过程中,如果无 **必需** 参数,可一个参数也不写,效果就是此事件任何情况下都会调用此方法。例如:`@CQMessage()`
|
||||
在使用注解绑定事件过程中,如果无 **必需** 参数,可一个参数也不写,效果就是此事件任何情况下都会调用此方法。例如:`@CQMessage()`
|
||||
|
||||
:::
|
||||
|
||||
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
|
||||
|
||||
@@ -61,39 +63,39 @@ QQ 收到消息后触发的事件对应注解。
|
||||
- 在用户 QQ 为 `123456` 的用户私聊给机器人发消息后机器人回复内容。
|
||||
- 用户发送文字为 `hello` 时返回 `你好啊,xxx` 的消息。
|
||||
|
||||
=== "代码"
|
||||
代码
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQMessage(message_type="private",user_id=123456)
|
||||
*/
|
||||
public function test() {
|
||||
return "你和机器人私聊发送了这些文本:".ctx()->getMessage();
|
||||
}
|
||||
/**
|
||||
* @CQMessage(message="hello")
|
||||
*/
|
||||
public function hello() {
|
||||
return "你好啊,".ctx()->getUserId();
|
||||
}
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQMessage(message_type="private",user_id=123456)
|
||||
*/
|
||||
public function test() {
|
||||
return "你和机器人私聊发送了这些文本:".ctx()->getMessage();
|
||||
}
|
||||
```
|
||||
/**
|
||||
* @CQMessage(message="hello")
|
||||
*/
|
||||
public function hello() {
|
||||
return "你好啊,".ctx()->getUserId();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
效果
|
||||
|
||||
<chat-box>
|
||||
) 假设我是私聊机器人
|
||||
( 你和机器人私聊发送了这些文本:假设我是私聊机器人
|
||||
^ 假设我现在切到群里,在群里发hello
|
||||
) hello
|
||||
( 你好啊,123456
|
||||
</chat-box>
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'假设我是私聊机器人'},
|
||||
{type:1,content:'你和机器人私聊发送了这些文本:假设我是私聊机器人'},
|
||||
{type:2,content:'假设我现在切到群里,在群里发hello'},
|
||||
{type:0,content:'hello'},
|
||||
{type:1,content:'你好啊,123456'},
|
||||
]"></chat-box>
|
||||
|
||||
## CQCommand()
|
||||
|
||||
@@ -125,11 +127,13 @@ QQ 收到消息后触发的事件对应注解。
|
||||
| group_id | `int64` 或 `string` | 限定消息发送来源群 ID,同 `@CQMessage` | 空 |
|
||||
| level | `int` | 事件优先级(越大越靠前) | 20 |
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
在 `@CQCommand` 注解事件中,从 `match` 到 `keyword` 六个参数中,必须且只能定义一个,`alias` 目前只能和 `match` 参数同时使用;
|
||||
|
||||
框架内部对于同一条消息事件,优先处理 `@CQCommand` 注解事件,如果未匹配到任何注解事件,则才会继续执行 `@CQMessage` 注解事件。
|
||||
在 `@CQCommand` 注解事件中,从 `match` 到 `keyword` 六个参数中,必须且只能定义一个,`alias` 目前只能和 `match` 参数同时使用;
|
||||
|
||||
框架内部对于同一条消息事件,优先处理 `@CQCommand` 注解事件,如果未匹配到任何注解事件,则才会继续执行 `@CQMessage` 注解事件。
|
||||
|
||||
:::
|
||||
|
||||
- 参数 `match` 匹配模式是:遇到空格、换行就会切分,比如 `点歌 xxx yyy` 会被分割为 `[点歌,xxx,yyy]`,然后抽取第一个词做为命令去匹配,剩下的为参数。
|
||||
- 参数 `pattern` 匹配模式是:\* 号位置变成参数,比如 `从*到*的随机数`,我们输入 `从1到9的随机数`,成功匹配,参数列表:`[1,9]`。
|
||||
@@ -141,52 +145,54 @@ QQ 收到消息后触发的事件对应注解。
|
||||
|
||||
我们以参数 `match` 写一个简单的 demo:
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
代码
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQCommand(match="疫情",alias={"COVID"})
|
||||
*/
|
||||
public function virus(){
|
||||
$city = ctx()->getNextArg("请输入城市名称");
|
||||
return "城市 ".$city." 的疫情状况如下:"."{这里假装是疫情接口返回的数据}";
|
||||
}
|
||||
/**
|
||||
* 如果选择使用 match 参数的话,可以省略 `match=`
|
||||
* @CQCommand("掷硬币")
|
||||
*/
|
||||
public function randChoice() {
|
||||
return "你看到的是:" . (mt_rand(0,1) ? "正面" : "反面");
|
||||
}
|
||||
/**
|
||||
* @CQCommand(pattern="*把*翻译成*")
|
||||
*/
|
||||
public function translate() {
|
||||
ctx()->getNextArg(); // 为什么需要单独调用一次呢?看下面例子就知道啦
|
||||
$text = ctx()->getNextArg(); // 获取第二个星号匹配的内容
|
||||
$target = ctx()->getNextArg(); // 获取第三个星号匹配的内容
|
||||
// 这里 FakeTranslateAPI 是假设我们对接了一个翻译的 API,开发时请替换为自己的接口。
|
||||
return "翻译结果:" . FakeTranslateAPI::translate($text, $target);
|
||||
}
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQCommand;
|
||||
|
||||
class Hello {
|
||||
/**
|
||||
* @CQCommand(match="疫情",alias={"COVID"})
|
||||
*/
|
||||
public function virus(){
|
||||
$city = ctx()->getNextArg("请输入城市名称");
|
||||
return "城市 ".$city." 的疫情状况如下:"."{这里假装是疫情接口返回的数据}";
|
||||
}
|
||||
```
|
||||
=== "效果"
|
||||
/**
|
||||
* 如果选择使用 match 参数的话,可以省略 `match=`
|
||||
* @CQCommand("掷硬币")
|
||||
*/
|
||||
public function randChoice() {
|
||||
return "你看到的是:" . (mt_rand(0,1) ? "正面" : "反面");
|
||||
}
|
||||
/**
|
||||
* @CQCommand(pattern="*把*翻译成*")
|
||||
*/
|
||||
public function translate() {
|
||||
ctx()->getNextArg(); // 为什么需要单独调用一次呢?看下面例子就知道啦
|
||||
$text = ctx()->getNextArg(); // 获取第二个星号匹配的内容
|
||||
$target = ctx()->getNextArg(); // 获取第三个星号匹配的内容
|
||||
// 这里 FakeTranslateAPI 是假设我们对接了一个翻译的 API,开发时请替换为自己的接口。
|
||||
return "翻译结果:" . FakeTranslateAPI::translate($text, $target);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<chat-box>
|
||||
) 疫情 北京
|
||||
( 城市 北京 的疫情状况如下:blablablabla
|
||||
) COVID 香港
|
||||
( 城市 香港 的疫情状况如下:blablablabla
|
||||
) 掷硬币
|
||||
( 你看到的是:正面
|
||||
) 我想把我爱你翻译成英语
|
||||
( 翻译结果:I love you!
|
||||
</chat-box>
|
||||
效果
|
||||
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'疫情 北京'},
|
||||
{type:1,content:'城市 北京 的疫情状况如下:blablablabla'},
|
||||
{type:0,content:'COVID 香港'},
|
||||
{type:1,content:'城市 香港 的疫情状况如下:blablablabla'},
|
||||
{type:0,content:'掷硬币'},
|
||||
{type:1,content:'你看到的是:正面'},
|
||||
{type:0,content:'我想把我爱你翻译成英语'},
|
||||
{type:1,content:'翻译结果:I love you!'},
|
||||
]"></chat-box>
|
||||
|
||||
## CQNotice()
|
||||
|
||||
@@ -292,47 +298,52 @@ TODO:先放着,有时间再更。
|
||||
|
||||
### 用法
|
||||
|
||||
=== "代码"
|
||||
代码
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQBefore;
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
class Test {
|
||||
/**
|
||||
* @CQBefore("message")
|
||||
*/
|
||||
public function filter(){
|
||||
// 可用于敏感词,如政治相关的词语不响应其他模块
|
||||
if(mb_strpos(ctx()->getMessage(), "谷歌") !== false) return false;
|
||||
else return true;
|
||||
}
|
||||
/**
|
||||
* @CQCommand("百科")
|
||||
*/
|
||||
public function wiki() {
|
||||
$content = ctx()->getNextArg("请说你要查百科的内容");
|
||||
// 这里假设你对接了一个查百科的接口
|
||||
return "已搜到匹配 $content 的如下结果:".FakeAPI::searchWiki($content);
|
||||
}
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\CQ\CQBefore;
|
||||
use ZM\Annotation\CQ\CQMessage;
|
||||
class Test {
|
||||
/**
|
||||
* @CQBefore("message")
|
||||
*/
|
||||
public function filter(){
|
||||
// 可用于敏感词,如政治相关的词语不响应其他模块
|
||||
if(mb_strpos(ctx()->getMessage(), "谷歌") !== false) return false;
|
||||
else return true;
|
||||
}
|
||||
```
|
||||
/**
|
||||
* @CQCommand("百科")
|
||||
*/
|
||||
public function wiki() {
|
||||
$content = ctx()->getNextArg("请说你要查百科的内容");
|
||||
// 这里假设你对接了一个查百科的接口
|
||||
return "已搜到匹配 $content 的如下结果:".FakeAPI::searchWiki($content);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
效果
|
||||
|
||||
<chat-box>
|
||||
) 百科 北京
|
||||
( 已搜到匹配 北京 的如下结果:blablabla
|
||||
) 百科 谷歌被封
|
||||
^ 机器人没有任何回复
|
||||
<chat-box :my-chats="[
|
||||
{type:0,content:'百科 北京'},
|
||||
{type:1,content:'已搜到匹配 北京 的如下结果:blablabla'},
|
||||
{type:0,content:'百科 谷歌被封'},
|
||||
{type:2,content:'机器人没有任何回复'},
|
||||
]"></chat-box>
|
||||
|
||||
!!! warning "注意"
|
||||
::: warning 注意
|
||||
|
||||
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
|
||||
|
||||
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
|
||||
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
|
||||
|
||||
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
|
||||
|
||||
使用 `ctx()->waitMessage()` 时等待用户输入下一条消息功能和 CQBefore 配合过滤消息时需注意,见 [FAQ - CQBefore 过滤不了 waitMessage](/FAQ/wait-message-cqbefore/)
|
||||
|
||||
:::
|
||||
|
||||
## CQAfter()
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
炸毛框架提供了一个简易但是高效易用的 HTTP 路由注解,你可以使用路由功能来开发任何 Web 应用微服务、API 接口、中间件等。
|
||||
|
||||
!!! quote "开发提示"
|
||||
::: tip 开发提示
|
||||
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不啰嗦。
|
||||
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不啰嗦。
|
||||
|
||||
:::
|
||||
|
||||
## 控制器和路由
|
||||
|
||||
@@ -62,50 +64,54 @@ public function testName($param) {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 路由示例
|
||||
|
||||
=== "代码"
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Controller;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
代码
|
||||
|
||||
```php
|
||||
<?php
|
||||
namespace Module\Example;
|
||||
|
||||
use ZM\Annotation\Http\Controller;
|
||||
use ZM\Annotation\Http\RequestMapping;
|
||||
/**
|
||||
* @Controller("/api")
|
||||
*/
|
||||
class Hello {
|
||||
/**
|
||||
* @Controller("/api")
|
||||
* @RequestMapping("/index")
|
||||
*/
|
||||
class Hello {
|
||||
/**
|
||||
* @RequestMapping("/index")
|
||||
*/
|
||||
public function index(){
|
||||
ctx()->getResponse()->end("This is API index page"); // 使用上下文获取响应对象
|
||||
}
|
||||
/**
|
||||
* @RequestMapping("/ping")
|
||||
*/
|
||||
public function ping(){
|
||||
return "pong"; // 直接返回字符串
|
||||
}
|
||||
public function index(){
|
||||
ctx()->getResponse()->end("This is API index page"); // 使用上下文获取响应对象
|
||||
}
|
||||
```
|
||||
/**
|
||||
* @RequestMapping("/ping")
|
||||
*/
|
||||
public function ping(){
|
||||
return "pong"; // 直接返回字符串
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "效果"
|
||||
效果
|
||||
|
||||
!!! example "效果描述"
|
||||
当访问浏览器的 `http://localhost:20001/api/index` 时,浏览器会返回 `This is API index page`,当访问 `/api/ping` 的 url 时,浏览器会返回 `pong`。
|
||||
|
||||
```
|
||||
/ -> 无任何路由
|
||||
/api/index -> Hello->index
|
||||
/api/ping -> Hello->ping
|
||||
```
|
||||
::: tip 效果描述
|
||||
|
||||
!!! tip "提示"
|
||||
当访问浏览器的 `http://localhost:20001/api/index` 时,浏览器会返回 `This is API index page`,当访问 `/api/ping` 的 url 时,浏览器会返回 `pong`。
|
||||
|
||||
当 `@Controller` 为 `/` 的时候,效果和不写是一样的,`@RequestMapping` 为 `/` 或 `/index/inside` 等多级路由也是可以的。
|
||||
```
|
||||
/ -> 无任何路由
|
||||
/api/index -> Hello->index
|
||||
/api/ping -> Hello->ping
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: tip 提示
|
||||
|
||||
当 `@Controller` 为 `/` 的时候,效果和不写是一样的,`@RequestMapping` 为 `/` 或 `/index/inside` 等多级路由也是可以的。
|
||||
|
||||
:::
|
||||
|
||||
### 绑定参数
|
||||
|
||||
@@ -126,46 +132,46 @@ public function index($arg) {
|
||||
|
||||
### 示例
|
||||
|
||||
=== "获取 GET"
|
||||
#### 获取 GET
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$get = ctx()->getRequest()->get;
|
||||
if(isset($get["name"])) return "hello, ".$get["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$get = ctx()->getRequest()->get;
|
||||
if(isset($get["name"])) return "hello, ".$get["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
=== "获取 POST(x-www-form-urlencoded)"
|
||||
#### 获取 POST(x-www-form-urlencoded)
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->post;
|
||||
if(isset($post["name"])) return "hello, ".$post["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->post;
|
||||
if(isset($post["name"])) return "hello, ".$post["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
=== "获取 JSON POST"
|
||||
#### 获取 JSON POST
|
||||
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->rawContent();
|
||||
$json = json_decode($post, true);
|
||||
if ($json === null) return "Invalid json data!";
|
||||
if(isset($json["name"])) return "hello, ".$json["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
```php
|
||||
/**
|
||||
* @RequestMapping("/testUrl")
|
||||
*/
|
||||
public function testUrl() {
|
||||
$post = ctx()->getRequest()->rawContent();
|
||||
$json = json_decode($post, true);
|
||||
if ($json === null) return "Invalid json data!";
|
||||
if(isset($json["name"])) return "hello, ".$json["name"];
|
||||
else return "Unknown name!!";
|
||||
}
|
||||
```
|
||||
|
||||
## 设置路由请求方式
|
||||
|
||||
|
||||
7
docs/faq/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# FAQ
|
||||
|
||||
这里会写一些常见的疑难解答,点击左侧问题名称打开对应解决方法。
|
||||
|
||||
如果框架运行过程中发现带有错误码(如 `E00034` 的形式),可以到 [错误码](/guide/errcode) 查看。
|
||||
|
||||
框架的常见问题见 [常见问题汇总](/faq/usual-question)。
|
||||
19
docs/faq/address-already-in-use.md
Normal 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 看不到框架相关进程,证明关闭成功,否则需要使用第一条强行杀死
|
||||
```
|
||||
22
docs/faq/display-deadlock.md
Normal 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 秒。
|
||||
5
docs/faq/light-cache-wrong.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 使用 LightCache 关闭时无法正常保存持久化
|
||||
|
||||
LightCache 因为是跨内存使用的,所以每次重启和关闭框架时,都只会让其中一个进程去保存。因为在 2.4.2 版本开始,持久化的逻辑发生了更改,不再支持 `expire = -2` 进行设置持久化(因为那样会很容易让开发者写错),仅支持使用 `LightCache::addPersistence($key)` 这样的方式进行设置持久化,所以在 2.4.2 版本以后,请使用此方法进行持久化设置,保证数据不丢失。
|
||||
|
||||
此外,2.4.2 版本起,不再支持用户手动调用 `savePersistence()` 方法,普通用户不可手动调用此方法,否则会导致数据出错。
|
||||
@@ -36,4 +36,4 @@
|
||||
|
||||
由于 2.0 框架使用了多进程模型,所以不能使用原先适用于单进程下全局变量的方式(ZMBuf)进行存取变量,所以 ZMBuf 下的所有方法都需要更改,其中 `get, set` 等对缓存操作的模型请根据 2.0 的文档变更使用 `Redis` 或内置的多进程共享内存可用的 `LightCache` 轻量缓存。
|
||||
|
||||
而获取全局配置文件,如 `global.php` 文件,也发生了变化,新框架引入了 `ZMConfig` 对象,可以快速地区分各类环境变量从而读取不同的配置文件。比如我们获取原先的 global 配置文件中的一项:`ZMBuf::globals("port")`,在 2.0 中需要使用 `ZMConfig::get("global", "port")` 方式。以此类推,`ZMBuf::config("xxx")` 也直接变为 `ZMConfig::get("xxx")` 了。
|
||||
而获取全局配置文件,如 `global.php` 文件,也发生了变化,新框架引入了 `ZMConfig` 对象,可以快速地区分各类环境变量从而读取不同的配置文件。比如我们获取原先的 global 配置文件中的一项:`ZMBuf::globals("port")`,在 2.0 中需要使用 `ZMConfig::get("global", "port")` 方式。以此类推,`ZMBuf::config("xxx")` 也直接变为 `ZMConfig::get("xxx")` 了。
|
||||
65
docs/faq/usual-question.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 框架常见问题(持续更新)
|
||||
|
||||
## 如何正确地强制退出炸毛框架?
|
||||
|
||||
首先要知道一个概念,炸毛框架和传统的 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`):
|
||||
|
||||

|
||||
|
||||
其中,`-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`:
|
||||
|
||||

|
||||