Compare commits

...

325 Commits
2.0.1 ... 2.7.3

Author SHA1 Message Date
Jerry Ma
b6e135a642 Merge pull request #74 from zhamao-robot/fix-onsetup-attribute-exec
Fix OnSetup Attribute execution bug (release 2.7.3)
2022-03-25 19:21:07 +08:00
crazywhalecc
d3f4ade215 Add Attribute and property (build 453, release 2.7.3) 2022-03-25 19:15:58 +08:00
crazywhalecc
56d2a52706 Fix OnSetup Attribute execution bug (build 452, release 2.7.3) 2022-03-25 19:02:43 +08:00
Jerry Ma
b40dc1e05e Merge pull request #71 from zhamao-robot/add-help-generator-docs
增加命令帮助生成器文档
2022-03-24 16:54:50 +08:00
Jerry Ma
bed3337866 Merge pull request #73 from zhamao-robot/integration-test-ignore-docs
修改集成测试配置以忽略文档变更
2022-03-24 16:40:48 +08:00
sunxyw
28dc73d060 Update integration-test.yml 2022-03-24 15:36:36 +08:00
sunxyw
5ea1226ad0 add help generator docs 2022-03-23 20:03:47 +08:00
sunxyw
e43f2a4158 update readme about php8 attribute (#70)
* update readme

* add docs about php8 attribute
2022-03-23 19:45:07 +08:00
sunxyw
d73e771ef3 fix help generator not working with alias (#69) 2022-03-23 19:44:16 +08:00
crazywhalecc
c077e9418c revert changes 2022-03-22 12:01:21 +08:00
crazywhalecc
2ce888c84c add robots.txt 2022-03-22 12:00:37 +08:00
crazywhalecc
b4d175ff98 fix docs problem 2022-03-22 11:52:12 +08:00
crazywhalecc
35e18d0481 add some irrelevant docs to expand white border 2022-03-22 01:26:53 +08:00
Jerry Ma
f6cccb00e3 Merge pull request #68 from zhamao-robot/document-upgrade
将文档重新部署和重构为 VuePress
2022-03-22 01:00:43 +08:00
Jerry Ma
2a82c82039 update to build 451
重构全局函数,统一函数命名,并补全注释
2022-03-22 01:00:20 +08:00
Jerry Ma
a2e8a1b582 update to build 451 2022-03-22 00:55:34 +08:00
crazywhalecc
dbd78d4b86 change ALL docs from MkDocs to VuePress!! 2022-03-22 00:51:03 +08:00
sunxyw
8105892b6e fix review 2022-03-21 23:28:57 +08:00
sunxyw
bc7d5871e4 replace deprecated functions 2022-03-21 20:49:09 +08:00
sunxyw
680e6a8c5f refactor global functions 2022-03-21 20:09:28 +08:00
Jerry Ma
c364084cb2 add command help generator (build 450)
增加命令帮助生成器 by @sunxyw
2022-03-21 14:13:56 +08:00
Jerry Ma
63f8042746 update to build 450 2022-03-21 12:52:48 +08:00
Jerry Ma
48e6b2cea0 update README.md [skip ci] 2022-03-21 05:04:37 +08:00
sunxyw
228309c571 fix cs 2022-03-21 04:46:54 +08:00
sunxyw
ca3a3df3ba add command help generator 2022-03-21 04:43:27 +08:00
crazywhalecc
c7df37b17c update install-runtime.sh 2022-03-21 02:58:59 +08:00
crazywhalecc
41f03fbba4 update docs 2022-03-21 01:25:26 +08:00
crazywhalecc
b3089c1bba add composer module support (build 449, 2.7.2) 2022-03-21 01:24:07 +08:00
crazywhalecc
c5a6f1fea4 update docs 2022-03-20 23:28:20 +08:00
crazywhalecc
ba2777137b let build command faster (build 448, 2.7.1) 2022-03-20 23:26:42 +08:00
crazywhalecc
6d90be164a update docs 2022-03-20 22:16:34 +08:00
crazywhalecc
4da6f5859a update to 2.7.0 release (build 447) 2022-03-20 22:12:58 +08:00
crazywhalecc
15d4ea710a add --no-state-check option (build 446) 2022-03-20 21:04:07 +08:00
crazywhalecc
f0541c1f32 Merge remote-tracking branch 'origin/master' 2022-03-20 19:05:54 +08:00
crazywhalecc
2b8cab1824 add AnnotationReader ignore name config (build 445, 2.7.0-beta5) 2022-03-20 19:05:13 +08:00
Jerry Ma
61c7972915 Update README.md 2022-03-20 17:09:24 +08:00
crazywhalecc
44a0eec74c change to integration-test 2022-03-20 17:00:37 +08:00
crazywhalecc
e57753e44b change to integration-test 2022-03-20 16:58:53 +08:00
crazywhalecc
74e91a2950 fix unpack autoload not working, change exclude_annotate to another name (build 444) 2022-03-20 16:51:48 +08:00
crazywhalecc
7ce3ef41df fix comment spacing problem (build 443) 2022-03-20 16:23:07 +08:00
crazywhalecc
444a77933a change autoload to hotload for phar hotload mode (build 442) 2022-03-20 16:20:14 +08:00
crazywhalecc
78f78c607d add packer namespace adjust (build 441) 2022-03-20 16:18:33 +08:00
crazywhalecc
69155002dc add site to gitignore 2022-03-20 16:13:51 +08:00
crazywhalecc
475d14fab7 update composer.json 2022-03-20 16:13:33 +08:00
crazywhalecc
f4d7e63358 update php cs fixer 2022-03-20 15:48:14 +08:00
crazywhalecc
f222d2b45b update composer requirement version 2022-03-20 15:43:31 +08:00
Jerry Ma
d0155fe1da Create code-style-analysis.yml 2022-03-20 15:35:10 +08:00
Jerry Ma
4737d0b507 Update and rename main.yml to mkdocs-deploy.yml 2022-03-20 15:23:05 +08:00
Jerry Ma
09bd0197bb Merge pull request #64 from sunxyw/patch-1
docs: add weather bot example
2022-03-20 15:20:59 +08:00
crazywhalecc
f0f120bd32 add Macroable (build 440) 2022-03-20 01:54:14 +08:00
crazywhalecc
c897da29c6 add PHP8 Attribute compatibility (build 439, 2.7.0-beta4) 2022-03-20 01:53:36 +08:00
sunxyw
e347e254e8 update docs index 2022-03-20 00:11:45 +08:00
crazywhalecc
12363aebf0 Merge remote-tracking branch 'origin/master' 2022-03-19 23:23:54 +08:00
crazywhalecc
ff0b925313 update docs 2022-03-19 23:23:14 +08:00
sunxyw
a6b4bd9b80 update weather bot example 2022-03-19 21:35:48 +08:00
sunxyw
485fa5476c add weather bot example 2022-03-19 21:25:10 +08:00
Jerry Ma
689076d97c Update mkdocs.yml 2022-03-19 20:33:13 +08:00
Jerry Ma
cca6102e91 Update README.md 2022-03-18 16:57:27 +08:00
Jerry Ma
095855162b Update mkdocs.yml 2022-03-18 16:48:30 +08:00
Jerry Ma
326f934013 Update mkdocs.yml 2022-03-18 16:44:04 +08:00
Jerry Ma
35b0c258fe Merge pull request #63 from sunxyw/patch-2
docs: add qingyunke chatbot integration example
2022-03-18 16:01:15 +08:00
sunxyw
6650846b15 update integrate-qingyunke-chatbot.md 2022-03-18 15:19:19 +08:00
sunxyw
a33d320f4c update integrate-qingyunke-chatbot.md 2022-03-18 02:54:23 +08:00
sunxyw
0bcfea6aa4 add qingyunke chatbot integration example 2022-03-18 02:30:45 +08:00
crazywhalecc
db6e63e91c fix Response class null error (build 438) 2022-03-17 20:48:09 +08:00
crazywhalecc
a7f84fb53a Merge remote-tracking branch 'origin/master' 2022-03-17 19:48:38 +08:00
crazywhalecc
ce7f2b1765 change ctx return force to ContextInterface (build 437) 2022-03-17 19:42:59 +08:00
Jerry Ma
abbfb59eff Merge pull request #62 from sunxyw/patch-2
docs: add missing module version example
2022-03-17 18:19:02 +08:00
sunxyw
b57fef16f9 add missing module version example 2022-03-17 01:53:06 +08:00
crazywhalecc
1706afbcd0 add cs fixer and PHPStan and activate it (build 436) 2022-03-15 18:05:33 +08:00
crazywhalecc
d01bd69aa5 update to 2.7.0-beta1 (build 435) 2022-03-13 22:50:32 +08:00
crazywhalecc
e6b9ae3ee1 add --watch function for no-installed-inotify users 2022-03-13 22:50:01 +08:00
crazywhalecc
73b6b8045d enhancement for process state 2022-03-13 22:47:11 +08:00
crazywhalecc
3c87abc6e8 enhancement for process state 2022-03-13 22:46:22 +08:00
crazywhalecc
e95925c129 add compatibility for PHP 8.1 2022-03-13 22:16:02 +08:00
crazywhalecc
82c44d6c40 enhancement for process state 2022-03-13 22:15:27 +08:00
crazywhalecc
e0a268e05e add autoload-dev auto scanner 2022-03-13 22:11:30 +08:00
crazywhalecc
487892e1d9 add force kill framework command --force 2022-03-13 22:05:53 +08:00
crazywhalecc
7ab4e88359 add KILLER PROMPT function 2022-03-13 22:05:23 +08:00
crazywhalecc
4702b6ee75 separate ProcessManager and WorkerManager 2022-03-13 22:04:52 +08:00
crazywhalecc
20cd3aa66d update docs 2022-03-13 22:03:52 +08:00
crazywhalecc
391114bdef update docs 2022-03-13 21:57:41 +08:00
crazywhalecc
ffe1052ecc add gitignore items 2022-03-13 21:54:43 +08:00
crazywhalecc
b4159152a7 add watcher 2022-01-08 20:19:10 +08:00
crazywhalecc
8fc6e4b0f7 update docs 2022-01-08 16:26:47 +08:00
crazywhalecc
c8938b7a4b update docs 2022-01-08 16:23:10 +08:00
crazywhalecc
34db1626a5 update to 2.6.6 (build 434) 2022-01-08 16:19:43 +08:00
crazywhalecc
7f0c97c5b9 update to 2.6.5 (build 433) 2021-12-28 22:40:40 +08:00
crazywhalecc
74050c46e7 update to 2.6.4 (build 432) 2021-12-25 19:21:41 +08:00
3ed1cb665a update to build 431 2021-12-22 14:39:46 +08:00
crazywhalecc
a6f33ba69d update to build 430 2021-12-08 21:29:38 +08:00
Jerry Ma
176b690417 Update v2.md 2021-12-07 13:28:21 +08:00
Jerry Ma
e22b1b90ec Update build-update.md 2021-12-07 13:27:33 +08:00
Jerry Ma
a3c560790c Merge pull request #55 from zhamao-robot/fix/cqafter
update to build 429
2021-12-07 13:25:55 +08:00
570e2108dd - 新增配置项 onebot.message_command_policy
- 新增 CQCommand 阻断策略的自定义配置功能
- 修复 CQAfter 无法正常使用的 bug #53
2021-12-07 13:21:29 +08:00
Jerry Ma
59c0d95e5d Create 2_Feature_request.yaml 2021-12-07 12:34:18 +08:00
Jerry Ma
9ceaecdc02 Update README.md 2021-12-07 12:17:47 +08:00
Jerry Ma
19d50898ef Create 1_Bug_report.yaml 2021-12-07 12:15:49 +08:00
Jerry Ma
5b62ca62ae Update README.md 2021-11-17 00:28:54 +08:00
fb528d30ce update docs 2021-11-16 16:51:05 +08:00
5f2d5ed334 update to build 428 2021-11-16 16:49:32 +08:00
c20e459900 update docs 2021-11-16 15:44:34 +08:00
09220825cf update to build 427 2021-11-16 15:41:01 +08:00
Jerry Ma
3d4db23d27 Update v2.md 2021-11-10 14:10:08 +08:00
Jerry Ma
4496b67dcc Update build-update.md 2021-11-10 14:09:18 +08:00
Jerry Ma
2a13298384 update to 2.5.8 (build 426) 2021-11-10 14:07:21 +08:00
Jerry Ma
d6ec404d76 Merge pull request #52 from YuFengZe/master
Update CQ.php
2021-11-10 14:06:13 +08:00
YuFengZe
3235fd4dc1 Update CQ.php 2021-11-08 22:05:41 +08:00
crazywhalecc
293740fee2 update to 2.5.7 (build 425) 2021-11-03 23:26:43 +08:00
crazywhalecc
d3c420ec84 update to build 424 (2.6.0-alpha1) 2021-11-02 16:01:24 +08:00
crazywhalecc
85ef09d43c Merge remote-tracking branch 'origin/master' 2021-11-01 00:29:11 +08:00
Jerry Ma
e4561d69c4 Merge pull request #45 from YuFengZe/master
Bug Fixed
2021-10-31 22:50:49 +08:00
Jerry Ma
6b4d206099 Update Hello.php 2021-10-31 22:50:12 +08:00
YuFengZe
50843edf6a Bug Fixed
解决发送“我是谁”却返回机器人信息的奇怪问题。
2021-10-31 22:01:14 +08:00
crazywhalecc
3a1686f8da update docs 2021-10-18 22:24:28 +08:00
crazywhalecc
66dd91bb97 update to 2.5.6 (build 423) 2021-10-17 23:55:02 +08:00
crazywhalecc
e020e5d593 update docs 2021-10-17 17:00:33 +08:00
crazywhalecc
3d62663281 update docs 2021-10-17 16:56:52 +08:00
crazywhalecc
8d9485c02e update docs 2021-10-17 15:37:55 +08:00
Jerry Ma
9fb45dd683 Update README.md 2021-10-15 09:38:07 +08:00
Jerry Ma
8a4924dba9 Update light-cache.md 2021-10-08 11:41:40 +08:00
crazywhalecc
beaf7be606 update to version 2.5.5 (buid 422) 2021-10-06 18:01:40 +08:00
Jerry Ma
7dbd21bdf4 Merge pull request #44 from furleywolf/patch-1
更新message-util的文档
2021-09-15 12:46:48 +08:00
furleywolf
9ce3056203 更新message-util的文档
发现漏掉了一个方法,还是补上去吧
2021-09-13 17:35:39 +08:00
crazywhalecc
880b4e847c update to version 2.5.4 (buid 421) 2021-09-11 16:27:16 +08:00
crazywhalecc
71e83d5bc8 update docs 2021-09-11 12:06:22 +08:00
crazywhalecc
432fd92cca update docs 2021-09-11 12:00:48 +08:00
crazywhalecc
907a9cea25 update to build 420 2021-09-11 11:59:02 +08:00
07391810ff update to build 419 2021-09-11 11:07:23 +08:00
5063421364 update docs 2021-09-10 11:28:56 +08:00
2b4d308783 update to build 418 2021-09-10 11:24:32 +08:00
e2f49968b3 Merge remote-tracking branch 'origin/master' 2021-09-01 14:14:13 +08:00
229778ebf9 update to build 417 2021-09-01 14:14:00 +08:00
Jerry Ma
d2c0972c93 Add actions badge [skip ci] 2021-08-31 13:40:29 +08:00
Jerry Ma
d300b6e518 Update main.yml 2021-08-31 13:39:11 +08:00
Jerry Ma
56cb7b2223 Update main.yml 2021-08-30 16:42:47 +08:00
Jerry Ma
2fc42d5d60 Update README.md 2021-08-30 16:40:53 +08:00
Jerry Ma
999e90f709 Update index.md 2021-08-30 16:17:13 +08:00
Jerry Ma
45c6cd7d2a Update main.yml 2021-08-30 16:12:35 +08:00
Jerry Ma
5aa0858021 Update main.yml 2021-08-30 16:05:15 +08:00
Jerry Ma
e981da3932 Update main.yml 2021-08-30 16:00:36 +08:00
Jerry Ma
2685be7306 Create main.yml 2021-08-30 11:27:51 +08:00
Jerry Ma
a13c4628f5 Update README.md 2021-07-15 11:37:35 +08:00
Jerry Ma
b36417454c Update README.md 2021-07-10 15:55:15 +08:00
crazywhalecc
a6f5952dee update to 2.5.1 (build 416) 2021-07-09 12:59:07 +08:00
crazywhalecc
d67dfe46f6 update to 2.5.0 (build 415) 2021-07-09 10:43:00 +08:00
crazywhalecc
e57cc43500 update to 2.5.0-b4 (build 414) 2021-07-09 02:15:04 +08:00
crazywhalecc
481063285b update some Docs and comments 2021-07-09 01:54:58 +08:00
crazywhalecc
d805523dbd update to 2.5.0-b3 (build 413) 2021-07-09 01:44:45 +08:00
crazywhalecc
58267f66fc update to 2.5.0-b3 (build 412) 2021-07-09 01:43:39 +08:00
crazywhalecc
48215f2e5e update to 2.5.0-b3 (build 411) 2021-07-09 01:39:45 +08:00
crazywhalecc
7e0fc1528a update to 2.5.0-b3 (build 410) 2021-07-09 01:38:30 +08:00
crazywhalecc
c185d20a93 update Docs 2021-07-04 18:02:03 +08:00
crazywhalecc
7ec847e576 update to 2.5.0-b2 (build 409) 2021-07-04 15:45:30 +08:00
jerry
4ee16d4fc6 update to 2.5.0-b1 (build 408) 2021-06-16 00:17:30 +08:00
Whale
59d614a24e Merge pull request #40 from sunxyw/patch-1
add latest config file format to quickstart-robot guide
2021-06-15 18:20:12 +08:00
sunxyw
40037b3f4b add highlight to quickstart-robot guide 2021-06-15 17:55:21 +08:00
sunxyw
f91c8b6205 add latest config file format to quickstart-robot guide
After the v1.0.0-beta2 update of go-cqhttp, the config file format has been converted from `hjson` to `yaml`. Update the guide.
See https://github.com/Mrs4s/go-cqhttp/releases/tag/v1.0.0-beta2
2021-06-15 17:46:00 +08:00
Whale
8f73a99ff7 Merge pull request #39 from YiwanGi/patch-1
Update global.php
2021-06-10 23:39:45 +08:00
Wang
e0f07cb396 Update global.php
🙃 强迫症 √
2021-06-10 17:45:27 +08:00
Whale
2950ab7472 Update README.md 2021-05-26 16:56:55 +08:00
Whale
0ab4053dfb Update README.md 2021-05-26 16:32:37 +08:00
Whale
745aa0f268 Update README.md 2021-05-26 16:31:41 +08:00
Whale
12bb93c2f0 Update README.md 2021-05-08 20:45:49 +08:00
95ca175901 Merge remote-tracking branch 'origin/master' 2021-05-08 10:06:17 +08:00
71585ed29d update to build 407
change daemon command from system to Process::kill
add master_pid for motd information
add option --preview
delete server_event_handler_class and use process
go-cqhttp-down.sh script add arm64 support
add ./zhamao support
change build-runtime.sh to install-runtime.sh
add option --disable-safe-exit
adjust some Console output
set start script using /bin/sh for supporting auto searching php and framework
2021-05-08 10:02:41 +08:00
Whale
231a377718 Update README.md 2021-05-08 01:24:27 +08:00
jerry
a80ee902a9 Merge remote-tracking branch 'origin/master' 2021-04-06 01:20:07 +08:00
jerry
c2d3b5f92a update to build 406 version 2021-04-06 01:19:56 +08:00
Whale
64365af124 Update README.md 2021-03-31 00:10:43 +08:00
jerry
60619dbffc update docs 2021-03-29 17:18:19 +08:00
jerry
77e77e9cc3 update to 2.4.4 version (build 405)
change requirements: add pcntl as required extension
update docs
2021-03-29 17:12:09 +08:00
jerry
d72b41a902 update to build 404
fix ./zhamao command
fix warning when first time starting framework
2021-03-29 15:48:47 +08:00
jerry
dfddaaea94 update docs 2021-03-29 15:40:36 +08:00
jerry
6b872c6f74 update to 2.4.3 version (build 403)
add config: swoole.max_wait_time (default 5)
add constant MAIN_WORKER
add getExpireTS() for LightCache
fix savePersistence() bug
add zm_go() to prevent errors
2021-03-29 15:34:24 +08:00
jerry
202c8aee77 update docs 2021-03-27 17:32:43 +08:00
jerry
ba397a1744 update docs and README.md
make image onto image server(for boost)
2021-03-27 17:30:39 +08:00
jerry
d699a152d5 update to 2.4.2 version (build 402)
change WORKING_DIR constant
change logic of savePersistence()
add LightCache addPersistence() and removePersistence() method
add `./zhamao` command
2021-03-27 16:30:15 +08:00
jerry
beef44ea50 update docs: fix picture 2021-03-25 17:22:07 +08:00
jerry
b991a2da7b update docs: fix picture 2021-03-25 17:20:00 +08:00
jerry
bbe4addd83 update to 2.4.1 version (build 401)
fix startup warning bug
2021-03-25 17:11:35 +08:00
jerry
600829645d fix init command 2021-03-25 16:56:08 +08:00
jerry
68280cfe7e fix init command 2021-03-25 16:50:32 +08:00
jerry
c5523aa95d reset global config 2021-03-25 16:19:09 +08:00
jerry
93a68a5582 update to v2.4.0 (build 400)
add systemd:generate command
add check:config command
init command add `--force|-F` option
add MessageUtil function `addShortCommand()`
clear debug message
2021-03-25 16:18:09 +08:00
jerry
6155236d3c update to v2.4.0 (build 399)
add CheckConfigCommand.php
add config update record docs
adjust swoole version to 4.5.0
fix stop and reload bugs
add $_running_annotation
add remote terminal
update global config
add timer tick exception handler
add zm_xxx global functions
add isAtMe(), splitCommand(), matchCommand() function for MessageUtil
add workerAction(), sendActionToWorker(), resumeAllWorkerCoroutines() functions for ProcessManager
optimize CQCommand match function
add custom TerminalCommand annotation
add TuringAPI
add getReloadableFiles() function for ZMUtil
2021-03-24 23:34:46 +08:00
28f7f20728 update docs 2021-03-23 14:51:55 +08:00
235256d679 rollback and correct to 398(v2.3.5) 2021-03-23 14:49:42 +08:00
626d569858 update composer and roll to 397(v2.3.4) 2021-03-23 14:16:56 +08:00
0492179bdd update composer and roll to 396(v2.3.3) 2021-03-23 14:13:04 +08:00
1dfd1de5c1 update composer and roll to 396(v2.3.3) 2021-03-23 14:11:21 +08:00
d15d01c32b update docs 2021-03-23 14:09:11 +08:00
jerry
c9f4278d9b update forgotten docs 2021-03-23 14:04:45 +08:00
jerry
6aa0540c9e Merge branch 'master' of https://github.com/zhamao-robot/zhamao-framework 2021-03-23 14:03:14 +08:00
jerry
9689dc9db1 rename 2021-03-23 14:02:58 +08:00
Whale
c20e3324d4 Merge pull request #35 from zhamao-robot/2.3.x
2.3.x
2021-03-23 13:28:18 +08:00
303f44cd2b update to version 2.3.2 (build 395)
fix overflow bug
2021-03-23 13:11:59 +08:00
66b73973b4 update to version 2.3.2 (build 394)
fix mysql error bugs
2021-03-23 12:47:00 +08:00
jerry
0ff4e52ed3 tmp connect 2021-03-22 07:44:11 +08:00
b6d1f724e9 update to build 389
add various global functions
2021-03-18 16:36:28 +08:00
e77b9d4970 update to 2.3.1 version (build 388)
cleanup code and fix a bug
2021-03-18 14:56:35 +08:00
jerry
456b102c15 update docs 2021-03-16 01:39:55 +08:00
jerry
cc57997abc update to build 387 2021-03-16 01:35:01 +08:00
jerry
19e61c7cc3 update to build 386
fix ZM_DATA equals null
add containsImage, getImageCQFromLocal function for MessageUtil
2021-03-16 01:34:17 +08:00
jerry
f908513dca update to build 385
add CQObject for CQ
add after-stop action(set terminal level 0)
update global.php modules, add http_proxy_server
add MessageUtil.php for message parsing
add RouteManager::addStaticFileRoute() for quick handling static file
finish onTask function finally!!
add TaskManager::runTask()
2021-03-15 02:54:16 +08:00
jerry
7dc39e6ada update to 2.2.11 verion
add build version (start from 384)
make 启动中 log as verbose
remove console input
fix pure http server bug
add error handler for zm_timer_tick and zm_timer_after
2021-03-13 15:16:10 +08:00
jerry
b0be53554d Merge remote-tracking branch 'origin/master' 2021-03-13 03:03:14 +08:00
jerry
b98048bd39 update docs 2021-03-13 03:03:01 +08:00
Whale
fffd3fdc95 Update README.md 2021-03-12 19:45:53 +08:00
jerry
dea9ed2ccd update docs 2021-03-08 00:56:35 +08:00
jerry
de5744c9e4 update to 2.2.10 version
add build-runtime.sh
remove debug msg when stopping
add --show-php-ver argument for server
2021-03-08 00:48:51 +08:00
jerry
a23f3d8f16 update to 2.2.9 version
update reply() to support quick operation
fix reload bug
fix reply() bug
2021-03-06 17:22:42 +08:00
jerry
0c24bfdedd fix a motd bug 2021-03-02 21:31:06 +08:00
jerry
c0b95c6840 delete workflows 2021-03-02 21:27:04 +08:00
jerry
e977b09e20 Merge remote-tracking branch 'origin/master' 2021-03-02 21:24:53 +08:00
jerry
4ff75cf199 update to 2.2.8 version
update motd message
2021-03-02 21:24:31 +08:00
Whale
24e70c70ce Update deploy-docs.yml 2021-03-02 14:27:55 +08:00
Whale
275a7bf00b Update deploy-docs.yml 2021-03-02 14:26:02 +08:00
Whale
455fc79818 Update deploy-docs.yml 2021-03-02 14:22:40 +08:00
Whale
8740c3c255 Update deploy-docs.yml 2021-03-02 14:19:51 +08:00
Whale
98bfca5bb9 Update deploy-docs.yml 2021-03-02 14:18:40 +08:00
Whale
fc8d01ad5f Update deploy-docs.yml 2021-03-02 13:53:12 +08:00
Whale
d9b8df1725 Update deploy-docs.yml 2021-03-02 13:50:52 +08:00
Whale
9b7802ac04 Update deploy-docs.yml 2021-03-02 13:50:39 +08:00
Whale
6e1f3e0406 Update deploy-docs.yml 2021-03-02 13:43:45 +08:00
Whale
a2d4bab062 Update index.md 2021-03-02 13:40:21 +08:00
Whale
f1cefad910 Create deploy-docs.yml 2021-03-02 13:37:07 +08:00
jerry
957c69bd1e update to 2.2.7 version
fix reply() bug
fix access_token bug
2021-02-27 16:19:18 +08:00
Whale
2902c5e805 Merge pull request #33 from YiwanGi/master
Update ServerEventHandler.php
2021-02-27 16:13:46 +08:00
Wang
faf9f5d988 Update ServerEventHandler.php
-When the token is incorrect, repeated connection events occur in the framework
2021-02-27 00:04:02 +08:00
Whale
874f061468 Update README.md 2021-02-26 11:05:04 +08:00
jerry
69521a1f1f cleanup code, update some features
add Hitokoto API
add Closure for access_token
add working_dir() global function
adjust reply() method to .handle_quick_operation
2021-02-24 23:37:00 +08:00
Whale
fb9dbed306 Merge pull request #32 from wen1014/master
warning bug fix
2021-02-23 23:24:45 +08:00
Whale
d42158ac90 Merge branch 'master' into master 2021-02-23 23:24:24 +08:00
Whale
ff3ebec562 Merge pull request #31 from YiwanGi/patch-8
Update spin-lock.md
2021-02-23 23:22:01 +08:00
wenhao
ea79de617e warning bug fix 2021-02-23 17:04:10 +08:00
Wang
9e1ad6a983 Update spin-lock.md
-Forgotten data content
2021-02-22 19:34:06 +08:00
Whale
c17248df31 Merge pull request #30 from YiwanGi/patch-7
Update light-cache.md
2021-02-22 11:33:52 +08:00
Whale
4c116ebd86 Merge pull request #29 from YiwanGi/patch-5
Update console.md
2021-02-22 11:33:29 +08:00
Whale
c490fe0c1c Merge pull request #28 from YiwanGi/patch-6
Update route-annotations.md
2021-02-22 11:32:46 +08:00
Whale
cefdf23799 Merge pull request #27 from YiwanGi/patch-4
Update README.md
2021-02-22 11:32:06 +08:00
Wang
7f70642606 Update light-cache.md
-Follow up the latest configuration data
2021-02-22 02:51:35 +08:00
Wang
1d5b2609f9 Update console.md 2021-02-22 02:03:22 +08:00
Wang
a206fe8b87 Update route-annotations.md
-Correction of typos
2021-02-22 01:18:12 +08:00
Wang
fb4f6c45ce Update README.md
-Detail optimization
2021-02-22 01:07:35 +08:00
jerry
c50ae245bd commitment, nothing 2021-02-21 22:17:34 +08:00
Whale
f6c2131ebf Merge pull request #26 from YiwanGi/patch-3
Update README.md
2021-02-21 22:15:39 +08:00
Whale
543d1d2922 Merge pull request #25 from YiwanGi/patch-2
Update basic-config.md
2021-02-21 22:14:27 +08:00
Whale
bb61e6f6a2 Merge pull request #24 from YiwanGi/patch-1
Update quickstart-robot.md
2021-02-21 22:13:06 +08:00
YiwanGi
2d1bbf6b48 Update README.md
-Adjust the display format appropriately
-Solve the problem of no access to images in China
2021-02-21 12:48:54 +08:00
YiwanGi
67e42cfe3e Update basic-config.md
-Better display
2021-02-21 11:28:01 +08:00
YiwanGi
429a2cf230 Update quickstart-robot.md
-Better display
2021-02-21 10:48:23 +08:00
jerry
9ace85e604 update to 2.2.5 version again
add transaction for SpinLock.php
add getAllCQ() for CQ.php
fix CQ bug
update docs
2021-02-20 16:57:19 +08:00
jerry
f677b0e132 update to 2.2.5 version
add saveToJson and loadFromJson function for DataProvider.php
fix @OnSave annotation not working
adjust swoole timer tick
add hasKey() for WorkerCache.php
2021-02-15 15:15:26 +08:00
jerry
f137f044d0 Merge remote-tracking branch 'origin/master' 2021-02-09 17:09:26 +08:00
jerry
77c12db31a reformat code 2021-02-09 17:09:09 +08:00
Whale
b670cb29fe Update README.md 2021-02-09 11:12:29 +08:00
Whale
95d7bb071d Update README.md 2021-02-09 10:54:59 +08:00
Whale
eadb4c1dee Update README.md 2021-02-09 10:54:05 +08:00
Whale
6672a6c852 Update README.md 2021-02-09 10:53:52 +08:00
Whale
094feddda4 Update README.md 2021-02-09 10:53:15 +08:00
Whale
f86eddb298 Update README.md 2021-02-09 10:48:05 +08:00
Whale
a93b4917cd Update README.md 2021-02-09 10:47:40 +08:00
jerry
0f9767aa16 update docs 2021-02-07 11:48:55 +08:00
jerry
0c9f246690 update to 2.2.4 version
update docs
fix broken ssh caused cpu overloading
fix WorkerCache bug when no global config
add global function zm_atomic
2021-02-07 11:46:42 +08:00
jerry
517d258d61 update to 2.2.3 version, I am tired
fix access_token not working
fix waitMessage() not working in v2.2.2
2021-01-30 00:06:42 +08:00
jerry
61e3818563 update to 2.2.2 version finally
clean redundant code
fix API reply in @OnTick for multi-process
fix loop error reporting
2021-01-29 23:34:34 +08:00
jerry
776ec98a3e fix waitMessage timeout bug 2021-01-29 22:32:29 +08:00
jerry
f3e844bb0a update to 2.2.2 version
fix QQBot error
clean code
2021-01-29 22:27:10 +08:00
jerry
a55cd4ed05 update docs 2021-01-29 21:37:02 +08:00
jerry
8a985620f9 update to 2.2.1 version
fix a compatibility bug
2021-01-29 21:36:14 +08:00
jerry
484ddf9dfa update Hello.php
update docs
2021-01-29 21:30:19 +08:00
jerry
b611b4aad6 add DaemonCommand for daemon players
adjust http_header available
2021-01-29 20:47:00 +08:00
jerry
b9f973c718 add unset for WorkerCache.php 2021-01-20 18:43:22 +08:00
jerry
cd6c971547 update Singleton 2021-01-20 16:45:50 +08:00
jerry
c68083484a update to 2.2.0 version
add OnPipeMessageEvent.php
add ProcessManager.php
add WorkerCache component
fix route bug
correct Exception to ZMException
2021-01-20 16:11:04 +08:00
jerry
f999e689bf update docs 2021-01-18 18:09:33 +08:00
jerry
187a08a621 update to 2.1.6 version
优化代码结构
增加更多提示语
修复处理空格消息时的报错
修复上下文的bug
2021-01-18 18:08:29 +08:00
Whale
c208298937 Update README.md 2021-01-14 18:32:54 +08:00
crazywhalecc
e9e3e5e129 update docs 2021-01-13 15:46:55 +08:00
crazywhalecc
1ef8225d10 update to 2.1.5 version
change route to Symfony routing
2021-01-13 15:40:27 +08:00
crazywhalecc
ccadec23e4 update docs and change console command suitable 2021-01-07 16:01:01 +08:00
jerry
0972a1959e update README.md 2021-01-05 23:35:49 +08:00
crazywhalecc
ce74191947 update docs 2021-01-05 16:19:35 +08:00
crazywhalecc
4feeb9519c update docs 2021-01-04 16:59:19 +08:00
crazywhalecc
efee146215 update docs 2021-01-04 16:45:06 +08:00
jerry
96ce7b30d0 add InterruptException catcher to onRequest 2021-01-04 01:35:54 +08:00
jerry
076339baec move warning to framework 2021-01-03 18:16:35 +08:00
jerry
50026be73d fix a bug 2021-01-03 18:13:44 +08:00
jerry
a1ad634926 fix a bug 2021-01-02 20:00:33 +08:00
jerry
0a5defaf29 fix a bug 2021-01-02 19:44:19 +08:00
jerry
557efc47a8 update to 2.1.4 version 2021-01-02 19:40:13 +08:00
jerry
c566f940e0 add custom app version 2021-01-02 18:10:20 +08:00
jerry
381062c6c5 update to 2.1.3 version
fix an AnnotationParser bug which is stupid
2021-01-02 16:36:25 +08:00
jerry
b31876025e update composer.json 2021-01-02 13:35:45 +08:00
jerry
ae8b0acdaa update to 2.1.2 version 2021-01-02 13:35:24 +08:00
jerry
20ca3e7416 update docs 2021-01-02 13:28:26 +08:00
jerry
a1bfc031b8 update to 2.1.1 version 2021-01-02 13:20:16 +08:00
jerry
8a6f8f54a5 update composer.json path 2021-01-02 13:19:39 +08:00
jerry
19f0bffcd8 update composer.json 2021-01-02 13:16:22 +08:00
jerry
6337b626d6 update to 2.1.0 version
add @OnCloseEvent, @OnOpenEvent, @OnMessageEvent, @OnRequestEvent
update EventDispatcher.php and change it to status-based schema
fix @CQBefore bugs
2021-01-02 13:15:50 +08:00
crazywhalecc
7434bac94e update to 2.0.3 version 2020-12-31 16:26:51 +08:00
crazywhalecc
ac45ab0dec update to 2.0.2 version 2020-12-31 13:58:59 +08:00
crazywhalecc
8d248f301e update to 2.0.2 version 2020-12-31 13:56:59 +08:00
jerry
937d31ccd9 update docs 2020-12-31 01:44:09 +08:00
crazywhalecc
ef263a5d1e update docs 2020-12-31 00:35:40 +08:00
jerry
bf03dfda38 Merge remote-tracking branch 'origin/master' 2020-12-31 00:28:36 +08:00
jerry
6ba209c4c7 update docs 2020-12-31 00:28:16 +08:00
Whale
e7d86537be Update README.md 2020-12-26 21:04:48 +08:00
crazywhalecc
44979c670f update docs 2020-12-25 16:53:44 +08:00
Whale
747ecf28ea Merge pull request #23 from 854854321/master
[MOD] add global function -> getAllFdByConnectType  根据连接类型获取所有fd.
2020-12-25 16:41:48 +08:00
Whale
f5d5929cb9 Update global_functions.php 2020-12-25 16:41:14 +08:00
Whale
6e866001d6 Update global_functions.php 2020-12-25 16:40:02 +08:00
wenhao
775c275288 [MOD] 解决php Notice <PHP Notice: Undefined index: x-self-id> 2020-12-25 16:28:21 +08:00
wenhao
86a0e1a2ca [MOD] add global function -> getAllFdByConnectType 根据连接类型获取所有fd. 2020-12-25 15:48:41 +08:00
Whale
aae79aacb5 Update README.md 2020-12-25 01:42:23 +08:00
Whale
991af3d728 Update README.md 2020-12-25 01:41:23 +08:00
Whale
25ae4e02c2 Update README.md 2020-12-25 01:40:45 +08:00
Whale
5a6642e217 Update README.md 2020-12-25 01:40:24 +08:00
Whale
d00f6ee3a2 Update README.md 2020-12-25 01:27:19 +08:00
crazywhalecc
1e6cc9cd84 update docs 2020-12-23 16:20:35 +08:00
crazywhalecc
b7b94db5ea Merge remote-tracking branch 'origin/master' into master 2020-12-23 16:15:18 +08:00
crazywhalecc
b0054d7884 update docs 2020-12-23 16:14:59 +08:00
Whale
22942c33cf Update README.md 2020-12-23 11:27:23 +08:00
crazywhalecc
1f9c5eeeb4 add CNAME 2020-12-23 11:23:57 +08:00
295 changed files with 21110 additions and 5105 deletions

View File

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

View File

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

View File

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

65
.github/workflows/integration-test.yml vendored Normal file
View 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
View 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 }}

24
.gitignore vendored
View File

@@ -2,7 +2,6 @@
/src/test/
/src/webconsole/config/
/vendor/
zm.json
/zm_data/
composer.lock
/resources/server.phar
@@ -10,3 +9,26 @@ composer.lock
/bin/.phpunit.result.cache
/resources/zhamao.service
.phpunit.result.cache
.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
View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -1,3 +0,0 @@
FROM zmbot/swoole:latest
# TODO: auto-setup entrypoint

103
README.md
View File

@@ -1,27 +1,29 @@
<div align="center">
<img src="/resources/images/logo_trans.png" height = "150" alt="炸毛框架"><br>
<img src="https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/logo_trans.png" width = "150" height = "150" alt="炸毛框架"><br>
<h2>炸毛框架</h2>
炸毛框架 (zhamao-frameowork) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
炸毛框架 (zhamao-framework) 是一个协程高性能的聊天机器人 + Web 服务器开发框架<br><br>
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)]()
[![作者QQ](https://img.shields.io/badge/作者QQ-627577391-orange.svg)](http://wpa.qq.com/msgrd?v=3&uin=627577391&site=qq&menu=yes)
[![zhamao License](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/zhamao-robot/zhamao-framework/blob/master/LICENSE)
[![Latest Stable Version](http://img.shields.io/packagist/v/zhamao/framework.svg)](https://packagist.org/packages/zhamao/framework)
[![Banner](https://img.shields.io/badge/CQHTTP-v11-black)]()
[![Banner](https://img.shields.io/badge/OneBot-v11-success)](https://github.com/howmanybots/onebot)
![Integration Test](https://github.com/zhamao-robot/zhamao-framework/actions/workflows/integration-test.yml/badge.svg)
[![stupid counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/stupid.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=stupid)
[![TODO counter](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
[![注解数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/AnnotationBase.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=AnnotationBase)
[![TODO 数量](https://img.shields.io/github/search/zhamao-robot/zhamao-framework/TODO.svg)](https://github.com/zhamao-robot/zhamao-framework/search?q=TODO)
</div>
## 开发者注意
**开发者 QQ 群670821194**
开发者 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,49 +42,76 @@ public function index() {
}
```
> 从 2.7.0 版本开始,框架已支持同时使用 Annotation 和原生 Attribute 注解,供开发者根据需要自由选用。
## 开始
框架首先需要部署环境,可以参考下方文档中部署环境和框架的方法进行。
## 文档v2 版本)
查看文档:[https://docs-v2.zhamao.xin/](https://docs-v2.zhamao.xin/)
如果你是初学者,可以直接使用以下脚本部署 PHP 环境和安装框架的脚手架:
备用链接:[http://docs-v2.zhamao.me/](http://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 多工作进程机制和协程加持,尽可能简单的情况下提升了性能
- 灵活的注解事件绑定机制
- 支持下断点调试Psysh
- 灵活的注解事件绑定机制,可同时使用 Annotation 和原生 Attribute 注解
- 易用的上下文,模块内随处可用
- 采用模块化编写,可单独拆装功能
- 常驻内存,全局缓存变量随处使用
- 自带 MySQL、Refis 等数据库连接池等数据库连接方案
- 自带 HTTP 服务器、WebSocket 服务器可复用,可以构建属于自己的 HTTP API 接口
- 静态文件服务器
- 采用模块化编写,可自由搭配其他 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。目前项目仅一人维护耗费精力较大所以非常欢迎对框架的贡献。
如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 后修改并提交 Pull Request。
目前项目仅一人维护,耗费精力较大,所以非常欢迎对框架的贡献。
本项目为作者闲暇时间开发,如果觉得好用,不妨进行捐助~你的捐助会让我更加有动力完善插件,感谢你的支持!
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。
我们会将捐赠的资金用于本项目驱动的炸毛机器人和框架文档的服务器开销上。[捐赠列表](https://github.com/zhamao-robot/thanks)
如果您不想直接参与框架的开发,也可以分享你编写的模块,帮助完善框架生态。
### 支付宝
![支付宝二维码](/resources/images/alipay_img.jpg)
如果你对我们的周边感兴趣,我们还有炸毛机器人定制 logo 的雨伞,详情咨询作者 QQ我们会作为您捐助了本项目
![支付宝二维码](https://cdn.jsdelivr.net/gh/zhamao-robot/zhamao-framework/resources/images/alipay_img.jpg)
## 关于
框架和 SDK 是 炸毛机器人 项目的核心框架开源部分。炸毛机器人是作者写的一个高性能机器人,曾获全国计算机设计大赛一等奖。
欢迎随时在 HTTP-API 插件群里提问,当然更好的话可以加作者 QQ627577391或提交 Issue 进行疑难解答
作者的炸毛机器人已从2018年初起稳定运行了**四年半**,并且持续迭代
欢迎随时在 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 动态,更新前请将自己的模块代码做好备份。
@@ -90,4 +119,12 @@ public function index() {
**注意**:在你使用 mirai 等 `AGPL-3.0` 协议的机器人软件与框架连接时,使用本框架需要将你编写或修改的部分使用 `AGPL-3.0` 协议重新分发。
![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg)
在贡献代码时,请保管好自己的全局配置文件中的敏感信息,请勿将带有个人信息的配置文件上传 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 中为项目开发规范化提出的一些建议。
<!-- ![star](https://starchart.cc/zhamao-robot/zhamao-framework.svg) -->

View File

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

View File

@@ -1,28 +1,34 @@
#!/usr/bin/env php
<?php
#!/bin/sh
use ZM\ConsoleApplication;
# shellcheck disable=SC2068
# shellcheck disable=SC2181
# author: crazywhalecc
# since: 2.5.0
// 这行是用于开发者自己电脑的调试功能
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
$symbol = sha1(is_file("/flag2") ? file_get_contents("/flag2") : '1') == '6252c0ec7fcbd544c3d6f5f0a162f60407d7a896' || mb_strpos(getcwd(), "/private/tmp");
result=$(echo "$1" | grep -E "module|build")
// 首先得判断是直接从library模式启动的框架还是从composer引入library启动的框架
// 判断方法:判断当前目录上面有没有 /vendor 目录,如果没有 /vendor 目录说明是从 composer 引入的
// 否则就是直接从 framework 项目启动的
if (!is_dir(__DIR__ . '/../vendor') || $symbol) {
define("LOAD_MODE", 1); //composer项目模式
define("LOAD_MODE_COMPOSER_PATH", getcwd());
/** @noinspection PhpIncludeInspection */
require_once LOAD_MODE_COMPOSER_PATH . "/vendor/autoload.php";
} elseif (substr(__DIR__, 0, 7) == 'phar://') {
define("LOAD_MODE", 2); //phar模式
// 会废弃phar启动的方式在2.0
} else {
define("LOAD_MODE", 0);
require_once __DIR__ . "/../vendor/autoload.php";
}
// 终端的命令行功能启动!!
$application = new ConsoleApplication("zhamao-framework");
$application->initEnv();
$application->run();
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

View File

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

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

View File

@@ -1,43 +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.0.1",
"extra": [],
"extra": {
"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",
"doctrine/annotations": "~1.10",
"php": "^7.2 || ^7.3 || ^7.4 || ^8.0 || ^8.1",
"ext-json": "*",
"psy/psysh": "@stable",
"symfony/polyfill-ctype": "^1.20",
"symfony/polyfill-mbstring": "^1.20",
"symfony/console": "^5.1",
"zhamao/connection-manager": "*@dev",
"zhamao/console": "^1.0",
"ext-posix": "*",
"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",
"symfony/routing": "^5.1",
"symfony/polyfill-php80": "^1.20"
"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": {
@@ -49,14 +68,26 @@
},
"autoload-dev": {
"psr-4": {
"ZMTest\\": "test/ZMTest"
},
"files": [
"test/ZMTest/Mock/mock.php"
]
"Module\\": "src/Module",
"Custom\\": "src/Custom"
}
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
},
"require-dev": {
"phpunit/phpunit": "^9.3",
"swoole/ide-helper": "@dev"
"brainmaestro/composer-git-hooks": "^2.8",
"friendsofphp/php-cs-fixer": "^3.2 != 3.7.0",
"phpstan/phpstan": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0",
"swoole/ide-helper": "^4.5"
},
"scripts": {
"post-install-cmd": [
"[ $COMPOSER_DEV_MODE -eq 0 ] || vendor/bin/cghooks add"
],
"analyse": "phpstan analyse --memory-limit 300M -l 0 ./src",
"cs-fix": "php-cs-fixer fix $1"
}
}
}

View File

@@ -1,117 +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' => 1024, //最多允许储存的条数需要2的倍数
'max_strlen' => 16384, //单行字符串最大长度需要2的倍数
'hash_conflict_proportion' => 0.6, //Hash冲突率越大越好但是需要的内存更多
'persistence_path' => $config['zm_data'].'_cache.json',
'auto_save_interval' => 900
];
/** 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'] = [
'X-Powered-By' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
'Server' => 'zhamao-framework',
'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;

View File

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

View File

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

1
docs/CNAME Normal file
View File

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

View File

@@ -1 +0,0 @@
# FAQ

40
docs/README.md Normal file
View 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 &nbsp;|&nbsp; Copyright © 2019-2022 Zhamao Developer Team &nbsp;|&nbsp; <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
```
## 效果图
![index_demo](/index_demo.png)

4
docs/advanced/README.md Normal file
View File

@@ -0,0 +1,4 @@
# 进阶开发
在本章,下面的部分将详细说明一些具体的案例和自定义框架的操作。
> 更多进阶教程敬请期待....(或者你可以选择提 Issue 到框架 GitHub有需求就写入文档

View File

@@ -0,0 +1,144 @@
# 接入 WebSocket 客户端
炸毛框架其实从本质上讲,就是一个 HTTP + WebSocket 服务器,所以框架也支持对接其他任何 HTTP 客户端和 WebSocket 客户端,实际上炸毛框架非常适合用 WebSocket 做在线的 IM 聊天通讯,也可以方便地进行 WS 通信。这里主要说明如何对接一个自定义的 WebSocket 客户端。
## 类型指定
由于 WebSocket 连接都具有同样的性质,没有状态,所以在建立 WebSocket 连接的时候,需要客户端表明自己的身份和类型。指定客户端连接类型的方式有两种:
- `GET` 参数传递,在连接的时候,加上 GET 参数 `type` 即可。比如 js 中 WebSocket 建立时地址写:`ws://127.0.0.1:20001/?type=foo`,这时传入的连接就是 `foo` 类型。
- `Header` 传递,用户需要在建立连接时指定 HTTP 的头部信息 `X-Client-Role`,例如 `X-Client-Role: foo`,这时传入的连接就是 `foo` 类型。
以上两种方式,`Header` 方式比 `GET` 方式优先级要高,如果两者均没有指定,框架会将此连接当作 `default` 类型接入。
::: tip 提示
对于对接 OneBot 标准的机器人客户端,只要符合 OneBot 标准,即 `X-Client-Role` 会自动带上 `universal``qq` 等字样,就会自动标记为 `qq` 类型。
:::
## 逻辑编写
传入连接后,我们就能通过注解事件绑定来做我们自己想做的事情了!比如下方是传入类型为 foo 连接要做的事情
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\Console\Console;
use ZM\ConnectionManager\ConnectionObject;
class Hello {
/**
* @OnOpenEvent("foo")
*/
public function onFooConnect(ConnectionObject $conn) {
Console::info($conn->getName()." 已连接!");
}
```
以上作用就是在终端输出 `foo 已连接!` 这个提示的。关于 `ConnectionObject` 对象,见下方。
## WS 连接对象
对于每一个 WebSocket 连接,框架内都有一个专属的操作类,有获取类型名称、保存链接参数和属性以及获取文件标识符等功能。
### getFd()
获取文件标示符,用于发送消息、接收消息等。这个参数获取的 `fd` 是 Swoole 指定的,用于发送信息等。
```php
$fd = $conn->getFd();
server()->send($fd, "hello world");
```
> WebSocket 是全双工的,所以发送和接收其实是互不干扰的,你可以不仅仅在 WebSocket 相关的上下文中,还可以比如在 HTTP 或者机器人上下文中给别的 WebSocket 客户端发请求。
### getName()
获取连接对象绑定的连接类型,例如上方提到的 `foo``default` 等。
```php
Console::info("当前连接类型:".$conn->getName()); //当前连接类型foo
```
### setName()
改变连接对象绑定的连接类型,例如从 `foo` 改为 `bar`
```php
$s = $conn->getName(); // foo
$conn->setName("bar");
$s = $conn->getName(); // bar
```
### getOptions()
获取此连接存储的所有参数,以数组形式。存储内容见下方 `setOption()`
格式:`["参数1" => {参数1的值}, "参数2" => {参数2的值}]`
### getOption()
获取此连接存储的参数,获取指定名称的,此方法拥有一个参数 `$key`,指定即可获取。
如果没有对应参数,则返回 `null`
我们在前面的机器人部分知道,框架主要是用于机器人的连接,那么机器人客户端在连接后,比如我们想知道这个机器人的 WS 连接对应的是哪个 QQ 号的机器人,我们就可以用 `getOption("connect_id")` 来获取。这个 `connect_id` 是 OneBot 标准的客户端接入后自动填入的一个参数。例如,我们想在机器人接入后打出接入机器人的 QQ 号:
```php
/**
* @OnOpenEvent("qq")
*/
public function onQQConnect($conn) {
Console::success("机器人 ".$conn->getOption("connect_id")." 已连接!"); // 机器人 123456 已连接!
}
```
### setOption()
设置连接存储的参数。参数:`setOption($key, $value)``$key` 限定为 `connect_id` 一种。(因为目前有了 LightCache所以这里暂时不提供别的 key 设定)
```php
$conn->setOption("connect_id", "asdasdasd"); // $value 最长长度为 29
```
## 发送到 WebSocket 客户端
很简单,从上面获取到 `fd` 后使用下面的方式就可以了~
```php
server()->push($conn->getFd(), "hello"); // 第二个为 string 类型的参数
```
## 从客户端接收
接收消息必须从 `@OnMessageEvent` 注解事件下接收,使用上下文 `ctx()->getFrame()` 获取消息帧。
从这里获取的 `Frame` 对象,见 [Swoole 文档 - Frame](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
Frame 对象有四个参数:
- `$frame->fd`:获取发来帧的 fd
- `$frame->data`:数据本体
- `$frame->opcode`:数据类型 int 值,见 [Swoole 文档 - 数据帧类型](https://wiki.swoole.com/#/websocket_server?id=%e6%95%b0%e6%8d%ae%e5%b8%a7%e7%b1%bb%e5%9e%8b)
- `$frame->finish`是否发送完毕bool
下面以接收一个 json 字符串为例,并进行后续的解析:
```php
/**
* @OnMessageEvent("foo")
*/
public function onMessage() {
$frame = ctx()->getFrame();
$json_str = $frame->data; // 假设传入的是 {"key1":"value1","k2":"v2"}
$json = json_decode($json_str, true);
Console::info("key1 的值是:" . $json["key1"]);
}
```
## 关闭连接
```php
server()->close($conn->getFd());
```

View File

@@ -0,0 +1,136 @@
# 框架高级启动
## 框架下载方式
从前面的几章中,我们了解到框架有多种下载到本地的方式。
- Composer 依赖模式
- Starter 从模板创建模式(等同于 Composer 模式)
- 源码模式
- Phar Composer 依赖模式
- Phar 源码模式
### Composer 依赖模式
从 Composer 依赖加载框架是一种拉取框架的方式,这种方式的优点在于,你可以直观地感受到是如何使用框架从零开始一个完整的项目的过程。
从 Composer 依赖的启动步骤:
```bash
mkdir my-bot # 新建一个空的文件夹
cd my-bot/
composer require zhamao/framework # 从 composer 拉取后会自动部署 autoload 和 composer.json 等内容
# 使用命令初始化框架
vendor/bin/start init
# 启动框架
vendor/bin/start server
```
注意:使用 `init` 命令时,会给当前目录解压以下文件:
```php
$extract_files = [
"/config/global.php", // 全局配置文件
"/.gitignore", // git 排除文件
"/config/file_header.json", // HTTP 文件头
"/config/console_color.json", // 终端颜色主题文件
"/config/motd.txt", // 框架启动时自定义的 motd
"/src/Module/Example/Hello.php", // 框架自带的示例模块
"/src/Module/Middleware/TimerMiddleware.php", // 框架自带的函数运行时间监控中间件
"/src/Custom/global_function.php" // 用户可在这里自定义编写自己的全局函数
];
```
经过 init 解压这些文件后,你的框架就能正常运行且开始编写代码了!
### Starter 模板模式
从模板新建其实原理和 Composer 依赖模式完全一样,只不过,这个过程是使用模板仓库新建的项目,使用 Composer 自带的 `create-project` 方式创建的。starter 也是一个 GitHub 项目,见 [地址](https://github.com/zhamao-robot/zhamao-framework-starter)。
```bash
composer create-project zhamao/framework-starter my-bot/ # my-bot 是你自定义的文件夹名称,和上方相同
cd my-bot
vendor/bin/start server # 启动框架
```
Starter 模式相当于直接从 GitHub 拉取 `zhamao-framework-starter` 项目,然后执行 `composer update`
那和 Composer 依赖模式有什么区别呢?没区别!构建出来的框架和文件是一模一样的!使用 Composer 依赖模式,使用 `init` 命令后,文件会和 `zhamao-framework-starter` 仓库拉取回来的模板一模一样!(或者换句话说,这个仓库就是使用 `init` 命令生成的文件的)
那使用哪种好呢?看你自己!如果你想给你自己的已有项目套上炸毛框架,那么就推荐使用 Composer 依赖模式,如果是从 0 开始编写框架模块,则推荐使用模板模式。
### 源码模式
源码模式和以上两种方案都不一样,源码模式允许你对框架本身进行一系列修改,框架本体就可以直接运行。
Composer 依赖模式(以及模板模式)和源码模式的区别是:
- 依赖模式和模板模式是通过 library 方式引入框架的,框架本身会放在 composer 的 `vendor/` 目录下,从 composer 引入的 library 相当于子集vendor 目录下的文件最好不要手动修改(应该都知道吧),所以框架本身也只是加载了进来。
- 源码模式相当于直接从框架源码目录运行框架和模块,框架源码都在 `src/ZM` 目录下,默认的示例模块都在 `src/Module` 下,是同级目录。而此时的 `vendor/` 目录只包含了框架依赖的外部组件,例如注解解析器和 psysh 等。
源码模式可以方便地调试和修改框架本身,拉取方式很简单,用 `git clone` 或从 GitHub 下载最新版的源码包解压即可。
```bash
git clone https://github.com/zhamao-robot/zhamao-framework.git
cd zhamao-framework/
bin/start server # 第一次运行时会提示一个“框架源码模式需要在autoload文件中添加Module目录为自动加载”
composer update # 更新 autoload 文件,应用刚才上一步添加的 `src/Module` 文件夹下的模块自动加载
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-coroutine`:关闭一键协程化。
- `--remote-terminal`:开启 nc 远程终端,配置文件使用全局中的 `remote_terminal` 项。也可以在全局配置中常开启status 设置为 true
- `--daemon`:以守护进程方式运行框架,此参数将直接在输出 motd 后将进程挂到 init 下运行,后台常驻。
- `--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、systemd 等方式挂后台跑则此命令不可用!
```bash
vendor/bin/start daemon:status # 查看守护进程的状态
vendor/bin/start daemon:reload # 重载框架
vendor/bin/start daemon:stop # 停止运行守护进程的框架
```
## 独立启动其他组件
框架默认不止启动框架的 `server` 命令,还有 `init` 命令和 `simple-http-server` 命令。`init` 命令在上方 Composer 依赖模式中提到过,就是初始化各个文件的。
### 独立 HTTP 文件服务器
如果你只需要一个静态文件服务器,类似 Nginx那么框架也支持。
```bash
vendor/bin/start simple-http-server your-web-dir/ --host=0.0.0.0 --port=8080
```
- `your-web-dir` 是必填的参数。
- `--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` 不一样,请勿同时混用,直接使用上述命令生成的配置文件即可正常使用!

View File

@@ -0,0 +1,229 @@
# 编写管理员专属功能
众所周知,如果大家使用炸毛框架来开发聊天机器人的话,会比较方便。但是有些地方你一定会感觉还是欠缺了点,比如下面这样,你想编写一个只能由机器人管理员,也就是你自己,才能触发的功能:
```php
/**
* @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 "禁言成功!";
}
```
这时候,如果只是自己有绝对的权利,可以将自己的 QQ 号写死在注解 `@CQCommand` 中,并限定 `user_id`(假设我的 QQ 号码为 123456
```php
/**
* @CQCommand(match="禁言",message_type="group",user_id=123456)
*/
```
但是,随着时间的推移,你的机器人伙伴群可能越来越大,这个命令可能不止需要绝对的你来使用,你还要将机器人的部分权利下发给更多的伙伴,怎么办呢?注解里面只能写死的。
答案很简单这时候我们就需要用到框架提供的中间件Middleware。中间件说白了就是在事件执行前、后、过程中抛出的异常对其进行阻断和插入代码比如我们上方在触发禁言这个注解事件前首先要判断执行这个命令的是不是钦定的管理员。
## 第一步:定义中间件
首先,我们需要定义一个中间件。在框架默认提供的脚手架中,包含了一个叫 `TimerMiddleware.php` 的示例中间件,这个示例中间件的目的是非常简单的,就是判断这个注解事件运行了多长时间。假设你有一个机器人功能,这个功能下的代码需要执行很长时间,可以使用这一注解轻松将事件执行的时间打印到终端上。
关于中间件的有关说明,见 [中间件](/event/middleware)。
下面我们假设你已经阅读过中间件注解的文档了,我们着手编写一个判断指令执行者是否是指定的管理员 QQ 的中间件。为了省事和让大家方便地复现,我先在脚手架下的目录 `src/Module/Middleware/` 下新建 PHP 类文件 `AdminMiddleware.php`(和 `TimerMiddleware.php` 在同一个目录)。
```php
<?php
namespace Module\Middleware;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Exception\ZMException;
use ZM\Http\MiddlewareInterface;
use ZM\Store\LightCache;
/**
* Class AdminMiddleware
* 示例中间件:用于动态管理一些管理员指令的中间件
* @package Module\Middleware
* @MiddlewareClass("admin")
*/
class AdminMiddleware implements MiddlewareInterface
{
/**
* @HandleBefore()
* @return bool
* @throws ZMException
*/
public function onBefore(): bool {
$r = ctx()->getUserId(); // 从上下文获取发消息的用户 QQ
$admin_list = LightCache::get("admin_list") ?? []; // 从轻量缓存获取管理员列表
return in_array($r, $admin_list); // 返回这个 QQ 是否在管理员列表中
}
}
```
其中,`@MiddlewareClass("admin")` 的意思是,定义这个类为名字叫 `admin` 的中间件,同时,所有中间件的类**必须**带上 `implements MiddlewareInterface`,统一接口形式。
`@HandleBefore()` 代表的是这个类下的这个函数onBefore被标注为这个中间件的 `onBefore` 事件,也就是说,如果有别的注解事件插入了这个 `admin` 中间件,那么执行对应注解事件前都要执行一下 `@HandleBefore` 所绑定的这个函数。而这个绑定的函数只能返回 `bool` 类型的值哦!
## 第二步:使用中间件
使用中间件很简单,在需要阻断的注解事件绑定的函数上再加一个注解就好了!我们以上方的禁言例子说明:
```php
/**
* @Middleware("admin")
* @CQCommand(match="禁言",message_type="group")
*/
```
<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`)的话,可以直接给类注解这个中间件,效果等同于给每一个函数写一次中间件注解。
```php
<?php
namespace Module\Example;
use ZM\Annotation\Http\Middleware;
/**
* Class AdminFunc
* @package Module\Example
* @Middleware("admin")
*/
class AdminFunc
{
// ...这里是你的一堆注解事件的函数
}
```
## 第三步:补全代码
上面我们讲到了,中间件里面使用了 `LightCache` 轻量缓存来储存临时的管理员列表,那么我们将这部分的代码完善吧!
**src/Module/Example/AdminFunc.php**
```php
<?php
namespace Module\Example;
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 "禁言成功!";
}
/**
* @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")
*/
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 :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>

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
# 框架剖析
## 框架运行总结构图
![](https://static.zhamao.me/images/docs/a23d8a952cf9c88d395888d220605a4f.png)

View File

@@ -1,3 +0,0 @@
# 进阶开发
## 深入
还没填坑,敬请期待!

View File

@@ -0,0 +1,67 @@
# 内部类文件手册
这个章节写明了在框架使用过程中可能涉及到的框架内部或 Swoole、其他 composer 依赖组件的内部类,这里会根据类的命名空间一一说明。
## Swoole\Http\Request
此类是 Swoole 内部的一个类,一般在收到 HTTP 请求时,在 `@RequestMapping``@OnRequestEvent()` 两个注解下可用,用作获取 GET、POST参数上传到后端的文件、Cookies 等。详见 [Swoole 文档 - Request](http://wiki.swoole.com/#/http_server?id=httprequest) 。
### 属性
- `$fd`:获取当前连接的文件描述符 ID。
- `$header``HTTP` 请求的头部信息。类型为数组,所有 `key` 均为小写。
- `$server``HTTP` 请求相关的服务器信息。
- `$cookie`:获取 Cookies。
- `$get`:获取 GET 参数。
- `$post`:获取 POST 参数。
- `$files`:获取上传的文件信息
### 方法
- `rawContent()`:获取 POST 包原始二进制内容,相当于原生 PHP 的 ` file_get_contents("php://input");`
- `getData()`:获取完整的原始 `Http` 请求报文。包括 `Http Header``Http Body`
### 示例
```php
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 ==> [
"我叫顺溜",
"我今年二十八"
]
*/
```

View File

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

View File

@@ -0,0 +1,72 @@
# 框架多进程
首先对于多进程概念,对于传统 PHP 程序员可能比较陌生,唯一接触到的地方可能就是 php-fpm 等一些方式处理时间长的请求时开进程去执行。关于多进程,我觉得廖雪峰的 Python 多进程这段讲的不错:
> Unix/Linux 操作系统提供了一个`fork()`系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是`fork()`调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
这里面的重点在于,多进程的创建,是父进程的复制,然后两个进程接下来运行的代码和存的内容就分道扬镳了。
PHP 也是如此,框架的多进程又是怎么一回事呢?为什么要采用多进程呢?
## 作用
使用过框架的你一定知道,框架是以命令行方式运行 PHP 的,而命令行方式运行 PHP就代表要常驻内存就像 Python、Node.js 一样。而默认情况下,比如 Python 的 Flask 为单线程单进程模式,也就是说同时只能处理一个 Web 请求。但大部分情况下,比如 Node.js提供的都是异步 I/O这也就是说明它在 Web 处理请求上,可同时承接的 I/O 密集型请求会更多一些,这样在对一般的 Web 应用中 I/O 密集型场景非常有用,而且往往只需要单进程也可以承载上万的并发请求。
在炸毛框架中,因为框架基于 Swoole 构建,所以天然支持协程,而协程就是针对 I/O 操作进行一个调度,类似异步的 Node.js所以针对项目中存在太多的 SQL 语句执行、文件读写的话,炸毛框架直接上手,无需做任何修改,也可以达到很好的性能。
**但是**CPU 密集型的应用怎么办呢?假设我的 Web 应用有大量的排序、md5 运算怎么办呢?这样的阻塞,假设是一个超级大的 for 循环或者是要执行很长时间的 while 循环CPU 一直在被占用。多进程就是针对 CPU 密集型的应用说 yes 的一个方案。
![diagram](https://static.zhamao.me/images/docs/06c17ab473f17ab10523a938cdbd8760.png)
我们假设现在有 3 个请求同时访问,也就是说上面的流程需要执行 3 遍。而如果我们只有一个进程的话,最后一个请求需要等待的时间为 `2*3+5*3=21` 秒,非常耗时。
而如果有两个进程处理 3 个请求,则最后一个完成的请求就缩短了,`2+5+2+5=14` 秒。
![diagram](https://static.zhamao.me/images/docs/dbb4e32e1c77f96162d10e41f25befa4.png)
所以如果要充分利用你的服务器或者个人电脑的多核 CPU 资源,就要设置多个进程来处理。一个进程只能在一个 CPU 上运行,而设置了多进程后,就可以让多核 CPU 充分运行多个进程,所以我们给框架设置多进程的推荐数值为等同于 CPU 的核心数。
## 为什么不是多线程
因为众所周知PHP 对线程的支持比较不好,而 ZTS 版本的 PHP 又会影响传统的 Web 端 PHP 的性能,再加上 Linux 对线程的切换效率和多进程切换的效率差不多,多线程容易造成数据读写不安全等问题,故 Swoole 使用的是多进程模型。
## 框架进程模型
![diagram](https://static.zhamao.me/images/docs/46a34feb0195d6ea12da7b80750c9e71.png)
上图中,横向的时间片可以理解为并行执行,这些操作在多个 CPU 内可能同时在执行。
## 进程间隔离
众所周知,进程是程序在操作系统中的一个边界,和自己有关的一切变量、内容和代码都在自己的进程内,不同进程之间如果不使用管道等方式,是不可以互相访问的。而加上开始描述的,创建子进程是一个复制自身的过程,所以也就会有如下图的情况:
![diagram](https://static.zhamao.me/images/docs/8b43e2179a63c8d91a508d7cefcd3226.png)
我们以静态类为例,设置一个进程中的全局变量。这里就会出现,同一个静态变量在多个进程中完全不同的值的结果。此后,我们将会在 Worker 进程中执行用户的代码,如果设置 Worker 数量仅为 1 的话,那么就简单许多了,你还是可以使用全局变量或静态类来存储你想要的内容而不用担心这种多个进程变量隔离的情况(因为用户的 Web 请求处理的代码只会在一个 Worker 进程中执行)。如果像上图一样设置了多个 Worker则用户过来的比如 HTTP 请求就有可能出现在不同的 Worker 进程中,给全局变量设值就一定会造成不同步的问题。这时我们就不可以使用全局变量做数据同步(注意,我说的是数据同步)。
## 跨进程同步
跨进程同步方案中,框架给出了很多种解决方案。
- MySQL 数据库
- Redis
- LightCache 轻量缓存(共享内存)
- WorkerCache 大缓存
- ZMAtomic 跨进程原子计数器
下面的表格我将列出下方的特点和各自的优缺点:
| 类型 | 用途 | 优点 | 缺点 |
| ----------- | --------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------ |
| MySQL | 大型的传统的关系式数据都可以用数据库,你懂的 | 就是数据库的优点 | 和数据库不在同一台服务器的话网络延迟会较大,数据获取效率不高 |
| Redis | 传统的 key-value 数据库 | 数据无同步等问题,性能高 | 有网络通信延迟 |
| LightCache | 框架封装的跨进程的 key-value 存储模型 | 性能强悍,无 I/O 和网络通信 | 需要提前分配最大内存大小,最大单个值长度大小,不灵活 |
| WorkerCache | 框架封装的基于进程的 key-value 存储模型,类似 Redis | 无需提前分配最大内存大小,受限于 PHP memory_limit | 见 WorkerCache 的说明 |
::: tip WorkerCache 的说明
对于 WorkerCache 来说其实是比较特殊的进程间通信。具体来说就是WorkerCache 的原理就是将变量指定的存到一个进程中,如果是本进程读写的话直接相当于改一下全局变量,如果是其他进程读写的话,则依靠进程间通信。
所以缺点也显而易见,如果使用过程中不是命中了 WorkerCache 存储所在的进程的话,则一直会使用进程间通信,影响一定的效率。
:::

3
docs/advanced/php-env.md Normal file
View File

@@ -0,0 +1,3 @@
# PHP 环境高级部署方式
TODO: 留个坑。

View File

@@ -0,0 +1,3 @@
# 使用 TaskWorker 进程处理密集运算
> TODO: 新开个坑有时间补上。__填坑标记__

View File

@@ -1,88 +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-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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

3
docs/component/README.md Normal file
View File

@@ -0,0 +1,3 @@
# 框架组件
这里列举了框架内的你可能会用到的常用组件。

View 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` 的相关章节。

View File

@@ -0,0 +1,550 @@
# 多媒体消息 - CQ码
消息中的多媒体内容使用 CQ 码来表示,形如 `[CQ:face,id=178]`。其中,`[CQ:]` 是固定格式;`face` 是「功能名」,除了 `face` 还有许多不同的功能名;`id=178` 是「参数」,某些功能不需要参数,而另一些需要多个参数,当有多个参数时,参数间使用逗号分隔。
## 格式
一些 CQ 码的例子如下:
```
[CQ:shake]
[CQ:face,id=178]
[CQ:share,title=标题,url=http://baidu.com]
```
更多 CQ 码功能请参考 [消息段类型](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md)。
::: warning 注意
CQ 码中不应有多余的空格,例如不应该使用 `[CQ:face, id=178]`
CQ 码的参数值可以包含空格、换行、除 `[],&` 之外的特殊符号等。在解析时,应直接取 `[CQ:` 后、第一个 `,``]` 前的部分为功能名,第一个 `,` 之后到 `]` 之间的部分为参数,按 `,` 分割后,每个部分第一个 `=` 前的内容为参数名,之后的部分为参数值。例如 `[CQ:share,title=标题中有=等号,url=http://baidu.com]` 中,功能名为 `share``title` 参数值为 `标题中有=等号``url` 参数值为 `http://baidu.com`
:::
## 转义
CQ 码中包含一些特殊字符:`[``]``,` 等,而 CQ 码又是可能混杂在纯文本内容之中的,因此消息中的纯文本内容需要对特殊字符进行转义,以避免歧义。具体的转义规则如下:
| 转义前 | 转义后 |
| ------ | ------- |
| `&` | `&amp;` |
| `[` | `&#91;` |
| `]` | `&#93;` |
另一方面CQ 码内部的参数值也可能出现特殊字符,也是需要转义的。由于 `,`(半角逗号)在 CQ 码中用于分隔参数,因此除了上面的转义规则,还需要对 `,` 进行转义,如下:
| 转义前 | 转义后 |
| ------ | ------- |
| `&` | `&amp;` |
| `[` | `&#91;` |
| `]` | `&#93;` |
| `,` | `&#44;` |
例如,一个链接分享消息的 CQ 码可能如下:
```
[CQ:share,title=震惊&#44;小伙睡觉前居然...,url=http://baidu.com/?a=1&amp;b=2]
```
## 封装调用
框架提供了 CQ 码的封装,你可以在任何位置使用封装好的 CQ 码生成器。
生成器是一个静态类,里面的方法全部是静态调用,命名空间是:`ZM\API\CQ`
例如,给用户发送图片这样写就好啦!只需要将添加图片的地方拼到回复用户的字符串里。如果只发图片,整个字符串里只能有 CQ 码。
```php
<?php
namespace Module\Example;
use ZM\API\CQ;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand("发送图片")
*/
public function msgRecv() {
return CQ::image("https://zhamao.xin/file/hello.jpg");
// 相当于返回:"[CQ:image,file=https://zhamao.xin/file/hello.jpg]"
}
}
```
效果:
<chat-box :my-chats="[
{type:0,content:'发送图片'},
{type:3,content:'https://zhamao.xin/file/hello.jpg'}
]"></chat-box>
## CQ 码操作
### CQ::decode()
CQ 码字符反转义。
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `&#44;` 转义为 `,`
| 反转义前 | 反转义后 |
| -------- | -------- |
| `&amp;` | `&` |
| `&#91;` | `[` |
| `&#93;` | `]` |
| `&#44;` | `,` |
```php
$str = CQ::decode("&#91;CQ:at,qq=我只是一条普通的文本&#93;");
// 转换为 "[CQ:at,qq=我只是一条普通的文本]"
```
### CQ::encode()
转义 CQ 码的敏感符号,防止 酷Q 把不该解析为 CQ 码的消息内容当作 CQ 码处理。
```php
$str = CQ::encode("[CQ:我只是一条普通的文本]");
// $str: "&#91;CQ:我只是一条普通的文本&#93;"
```
定义:`CQ::encode($msg, $is_content = false)`
`$is_content` 为 true 时,会将 `,` 转义为 `&#44;`
### CQ::escape()
`CQ::encode()`
### CQ::removeCQ()
去除字符串中所有的 CQ 码。
```php
$str = CQ::removeCQ("[CQ:at,qq=all]这是带表情的全体消息[CQ:face,id=8]");
// $str: "这是带表情的全体消息"
```
### CQ::getCQ()
解析 CQ 码。
- 定义:`getCQ($msg, $is_object = false)`
- 参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject` 对象此对象的属性和下表相同。2.3.0+ 版本可用)
- 返回:`数组 | CQObject | null`,见下表。
| 键名 | 说明 |
| ------ | ------------------------------------------------------------ |
| type | CQ码类型比如 `[CQ:at]` 中的 `at` |
| params | 参数列表,比如 `[CQ:image,file=123.jpg,url=http://a.com/a.jpg]`params 为 `["file" => "123","url" => "http://a.com/a.jpg"]` |
| start | 此 CQ 码在字符串中的起始位置 |
| end | 此 CQ 码在字符串中的结束位置 |
### CQ::getAllCQ()
定义:`CQ::getAllCQ($msg, $is_object = false)`
参数 `$is_object` 为 true 时,返回一个 `\ZM\Entity\CQObject[]` 对象数组此对象的属性和上面的表格内相同。2.3.0+ 版本可用)
解析 CQ 码,和 `getCQ()` 的区别是,这个会将字符串中的所有 CQ 码都解析出来,并以同样上方解析出来的数组格式返回。
```php
CQ::getAllCQ("[CQ:at,qq=123]你好啊[CQ:at,qq=456]");
/*
[
[
"type" => "at",
"params" => [
"qq" => "123",
],
"start" => 0,
"end" => 13,
],
[
"type" => "at",
"params" => [
"qq" => "456",
],
"start" => 17,
"end" => 30,
],
]
*/
```
## CQ 码列表
### CQ::face() - 发送 QQ 表情
发送 QQ 原生表情。
定义:`CQ::face($id)`
参数:`$id` 为 QQ 表情对应的 ID 号,一些常见的表情 ID 对应的表情样式见 [QQ 对应表情ID表](https://static.zhamao.me/face_id.html)。
```php
/**
* @CQCommand("打盹")
*/
public function faceTest() {
ctx()->reply("正在打盹...");
ctx()->reply(CQ::face(8));
}
```
<chat-box :my-chats="[
{type:0,content:'打盹'},
{type:1,content:'正在打盹...'},
{type:3,content:'https://docs-v1.zhamao.xin/face/8.gif'},
]"></chat-box>
::: tip 提示
对于不断更新的 QQ 版本下,可能会持续扩充新的 QQ 表情,如果上表没有新的表情的话,也可以使用消息接收的方式,让机器人收到表情后解析出来对应的 id 然后再发送。
:::
### CQ::image() - 发送图片
发送图片。
定义:`image($file, $cache = true, $flash = false, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 图片文件名 |
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的图片文件名直接发送外,还支持:
- 绝对路径,例如 `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==
```
### CQ::record() - 发送语音
发送语音消息。
定义:`CQ::record($file, $magic = false, $cache = true, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------- | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 音频文件名 |
| `flash` | ✓ | ✓ | `false` | 图片类型,当参数为 true 时代表发送闪照 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的语音文件名直接发送外,还支持其它形式,参考上方发送图片。
```php
/**
* @CQCommand("说你好")
*/
public function say() {
ctx()->reply(CQ::record("https://zhamao.xin/file/hello.mp3"));
}
```
<chat-box :my-chats="[
{type:0,content:'说你好'},
{type:1,content:'[语音消息,点击收听] 2\'\' )))'},
]"></chat-box>
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::at() - 群里@某人或全体
在群里 at 某个人或全体成员(全体成员需要有管理员权限)。
定义:`CQ::at($qq)`
参数:`$qq` 参数必填,如果填的是 QQ 号,则是单独 at 某人,如果是 `all`,则是 at 全体成员。
```php
/**
* @CQCommand("at测试")
*/
public function atTest() {
ctx()->reply(CQ::at(627577391)." 你好啊!");
}
```
<chat-box :my-chats="[
{type:0,content:'at测试'},
{type:1,content:'@鲸鱼 你好啊!'},
]"></chat-box>
### CQ::video() - 发送短视频
发送短视频。
定义:`CQ::video($file, $cache = true, $proxy = true, $timeout = -1)`
参数
| 参数名 | 收 | 发 | 默认值 | 说明 |
| --------- | ---- | ---- | ------ | ------------------------------------------------------------ |
| `file` | ✓ | ✓ | 必填 | 短视频文件名 |
| `cache` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否使用已缓存的文件,默认 `true` |
| `proxy` | | ✓ | `true` | 只在通过网络 URL 发送时有效,表示是否通过代理下载文件(需通过环境变量或配置文件配置代理),默认 `true` |
| `timeout` | | ✓ | `-1` | 只在通过网络 URL 发送时有效,单位秒,表示下载网络文件的超时时间,默认 -1 不超时 |
发送时,`file` 参数除了支持使用收到的视频文件名直接发送外,还支持其它形式,参考上方发送图片。
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::rps() - 猜拳
定义:`CQ::rps()`
用法:`CQ::rps()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::dice() - 掷骰子
定义:`CQ::dice()`
用法:`CQ::dice()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::shake() - 窗口抖动
定义:`CQ::shake()`
用法:`CQ::shake()`
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::poke() - 戳一戳
发送戳一戳。
定义:`CQ::poke($type, $id, $name = "")`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | ------------------------------------------------------------ | ------ |
| `type` | ✓ | ✓ | 见 [Mirai 的 PokeMessage 类](https://github.com/mamoe/mirai/blob/f5eefae7ecee84d18a66afce3f89b89fe1584b78/mirai-core/src/commonMain/kotlin/net.mamoe.mirai/message/data/HummerMessage.kt#L49) | 类型 |
| `id` | ✓ | ✓ | 同上 | ID |
| `name` | ✓ | | 同上 | 表情名 |
例子:`CQ::poke(6,-1)`
效果:放大招
> 此 CQ 码只能用于单独一条文本消息中,如果混有其他字符串,则会吞掉其他字符串内容。
### CQ::anonymous() - 匿名发消息
匿名发消息。需要在允许匿名发消息的群里发。
::: tip 提示
当收到匿名消息时,需要通过 [消息事件的群消息](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md#群消息) 的 `anonymous` 字段判断。
:::
定义:`CQ::anonymous($ignore = 1)`
```php
/**
* @CQCommand("匿名测试")
*/
public function anonymousTest() {
ctx()->reply(CQ::anonymous()."匿名测试");
}
```
### CQ::share() - 链接分享
发送链接分享卡片,可自定义内容。
定义:`CQ::share($url, $title, $content = null, $image = null)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --------- | ---- | ---- | -------- | -------------- |
| `url` | ✓ | ✓ | - | URL |
| `title` | ✓ | ✓ | - | 标题 |
| `content` | ✓ | ✓ | - | 可选,内容描述 |
| `image` | ✓ | ✓ | - | 可选,图片 URL |
```php
/**
* @CQCommand("链接分享测试")
*/
public function shareTest() {
ctx()->reply(CQ::share("https://baidu.com", "UC忽悠部", "震惊!我市一男子在光天化日之下..."));
}
```
### CQ::contact() - 推荐好友
发送推荐好友的卡片。
定义:`CQ::contact($type, $id)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | ------------- | ---------------------- |
| `type` | ✓ | ✓ | `qq``group` | 推荐好友或群 |
| `id` | ✓ | ✓ | - | 被推荐人的 QQ 号或群号 |
```php
/**
* @CQCommand("我的名片")
*/
public function myCard() {
ctx()->reply(CQ::contact("qq", ctx()->getUserId()));
}
```
### CQ::location() - 发送位置
发送位置,基于经纬度坐标发的。
定义:`CQ::location($lat, $lon, $title = "", $content = "")`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| --------- | ---- | ---- | -------- | -------------- |
| `lat` | ✓ | ✓ | - | 纬度 |
| `lon` | ✓ | ✓ | - | 经度 |
| `title` | ✓ | ✓ | - | 可选,标题 |
| `content` | ✓ | ✓ | - | 可选,内容描述 |
### CQ::music() - 音乐分享
分享音乐,通过卡片。
发送音乐分享卡片。此 CQ 码如果伴随着其他文字,则文字内容会被丢弃。
定义:`CQ::music($type, $id_or_url, $audio = null, $title = null, $content = null, $image = null)`
- `$type`: 发送类型
- `$id_or_url`: 音乐的 id 或 音乐卡片点进去打开的链接
- `$audio`: 音频文件的 HTTP 地址
- `$title`: 音乐卡片的标题,建议 12 字以内
- `$content`: 音乐卡片的简介内容(可选)
- `$image`: 音乐卡片的图片的链接地址(可选)
如果 `$type` 参数为 `qq``163``xiami`,则必须且只和第二个参数 `$id_or_url` 配合使用。这三个为内置分享,需要先通过搜索功能获取对应平台歌曲的 id 后使用。
如果 `$type` 参数为 `custom`,则表明此音乐卡片为用户自定义,你可以根据自己的需要自定义卡片内容和音频。此时必须填写 `$id_or_url`, `$audio`, `$title` 三个参数。
```php
ctx()->reply(CQ::music("163", "730806")); //一首我喜欢的歌
// 以内置的发送类型发送音乐卡片,我这里挑了网易云音乐的一首歌。
ctx()->reply("custom", "https://baidu.com/", "https://zhamao.xin/file/hello.mp3", "我是Siri说出来的Hello", "不服来打我呀!", "https://zhamao.xin/file/hello.jpg");
// 自定义整个卡片的每个内容
```
### CQ::forward() - 合并转发
合并转发消息。
定义:`CQ::forward($id)`
参数
```
[CQ:forward,id=123456]
```
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ------ | ---- | ---- | -------- | ------------------------------------------------------------ |
| `id` | ✓ | | 必填 | 合并转发 ID需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取具体内容 |
### CQ::node() - 合并转发自定义节点
接收时,此消息段不会直接出现在消息事件的 `message` 中,需通过 [`get_forward_msg` API](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/public.md#get_forward_msg-获取合并转发消息) 获取。发送时,通过获取回来的 API 节点信息进行发送。
定义:`CQ::node($user_id, $nickname, $content)`
参数
| 参数名 | 收 | 发 | 可能的值 | 说明 |
| ---------- | ---- | ---- | -------- | ------------------------------------------------------------ |
| `user_id` | ✓ | ✓ | - | 发送者 QQ 号 |
| `nickname` | ✓ | ✓ | - | 发送者昵称 |
| `content` | ✓ | ✓ | - | 消息内容,支持发送消息时的 `message` 数据类型,见 [API 的参数](https://github.com/howmanybots/onebot/blob/master/v11/specs/api/#参数) |
```php
/**
* @CQCommand("node测试")
*/
public function nodeTest() {
ctx()->reply(CQ::node(123456, "Jack", "[CQ:face,id=123]哈喽~"));
}
```
### CQ::xml() - XML 消息
发送 QQ 兼容的 XML 多媒体消息。
定义:`CQ::xml($data)`
参数:`$data` 为 xml 字符串
```php
/**
* @CQCommand("xml测试")
*/
public function xmlTest() {
ctx()->reply(CQ::xml("<?xml ..."));
}
```
### CQ::json() - JSON 消息
发送 QQ 兼容的 JSON 多媒体消息。
定义:`CQ::json($data, $resid = 0)`
参数同上,内含 JSON 字符串即可。
其中 `$resid` 是面向 go-cqhttp 扩展的参数,默认不填为 0走小程序通道填了走富文本通道发送。
::: tip 提示
因为某些众所周知的原因XML 和 JSON 的返回不提供实例,有兴趣的可以自行研究如何编写,文档不含任何相关教程。
:::
### CQ::_custom() - 扩展自定义 CQ 码
用于兼容各类含有被支持的扩展 CQ 码,比如 go-cqhttp 的 `[CQ:gift]` 礼物类型。
定义:`CQ::_custom(string $type_name, array $params)`
| 参数名 | 说明 |
| ----------- | --------------------------------------------------- |
| `type_name` | CQ 码类型,如 `music``at` |
| `params` | 发送的 CQ 码中的参数数组,例如 `["qq" => "123456"]` |
下面是一个例子:
```php
CQ::_custom("at",["qq" => "123456","qwe" => "asd"]);
// 返回:[CQ:at,qq=123456,qwe=asd]
```

View 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);
}
```

View 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]"
```

View File

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

View File

@@ -0,0 +1,808 @@
# 机器人动作 - V11
OneBotV11 类是封装好的 OneBot 标准的 API 接口调用类,可以在机器人连接后通过连接或者机器人 QQ 号获取对象并调用接口(如发送群消息、获取群列表等操作)。
| 属性项 | 属性值 | 备注 |
| -------- | ------------------ | -------------------------------- |
| 名称 | 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、设置协程返回还是异步返回结果等。
### OneBotV11::API_NORMAL
以默认(无后缀)方式请求 API。
### OneBotV11::API_ASYNC
以后缀 `_async` 方式异步请求 API。
### OneBotV11::API_RATE_LIMITED
以后缀 `_rate_limited` 方式请求 API。
## 方法
### setPrefix()
设置后缀。目前支持 `_async``_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)。
### setCallback()
设置 API 结果返回方式。默认为 true就是直接通过框架处理后接收回包直接返回给结果。如果为 false则 API 请求后只返回是否成功推送出 WS 数据包。
### getSelfId()
获取当前对象的机器人 QQ 或 OneBot 实例的 ID。
```php
$bot = OneBotV11::get(123456);
echo $bot->getSelfId(); //123456
```
### OneBotV11::get()
静态方法,用来通过机器人 QQ 或 OneBot 实例的 ID 获取 OneBotV11对象。
参数:`$robot_id`,必填。
```php
$r = OneBotV11::get(123456);
$r->sendPrivateMsg(55555, "hello");
```
### OneBotV11::getRandom()
静态方法,随机获取一个连接到框架的机器人(多个机器人实例连接到框架时适用)。
如果框架没有连接到任何机器人实例,则会抛出一个异常:`ZM\Exception\RobotNotFoundException`
```php
try {
$bot = OneBotV11::getRandom();
$bot->sendPrivateMsg(55555, "foo");
} catch (\ZM\Exception\RobotNotFoundException $e) {
echo "还没有机器人连接到框架!\n";
}
```
### OneBotV11::getAllRobot()
获取所有连接到框架的机器人的 OneBotV11 对象。
返回值:`OneBotV11[]`
```php
$all = OneBotV11::getAllRobot();
foreach($all as $v) {
$v->sendPrivateMsg(55555, "机器人轮流给一个人发消息啦!");
}
```
### __construct()
构造方法。
参数:`$connection`:炸毛框架内部的连接对象,必填参数。
```php
//从上下文获取 Websocket 连接对象
$conn = ctx()->getConnection();
$bot = new OneBotV11($conn);
```
## 返回结果处理
因为框架的机器人是兼容 OneBot 标准的(原 CQHTTP所以每次接收发送 API 请求的结果都是大体一样的结构。我们以 `sendPrivateMsg()` 为例,因为发送出去的每一条消息都会在 OneBot 实例(如 CQHTTP 插件、go-cqhttp 等)中对应一个消息 ID以供我们核查消息和后续撤回等操作需要。
```php
$bot = OneBotV11::get("123456"); // 机器人QQ号
$obj = $bot->sendGroupMsg("234567", "你好");
echo json_encode($obj, 128|256);
```
输出结果
```json
{
"status": "ok",
"retcode": 0,
"data": {
"message_id": 1243
}
}
```
如上,`$obj` 就是我们的回包内容,我们通过调用 `sendPrivateMsg()` 这个 API 后,会拿到机器人发送此条消息的消息 ID然后可以通过它来进行其他操作例如撤回
```php
$result = $bot->deleteMsg($obj["data"]["message_id"]);
vardump($result["retcode"]); //如果成功撤回,输出 int(0)
```
### 状态码和 Data
状态码一般情况成功都是 0 或者 200在过去炸毛框架兼容 CQHTTP 插件时,错误码的标准按照 CYKU 给出的标准编写,不同的 OneBot 标准的实现,可能有不同的数值,需要根据你对接的机器人客户端进行适配。
结果中返回的 `data` 字段根据下方不同 API 的调用而不同,具体查看每个 API 写明的 `响应数据` 表格。
### response 表
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ---------- | ----------------------- |
| `status` | String | "ok" | 状态码说明 |
| `retcode` | number | 0 | 返回状态码 |
| `data` | array | 见 data 表 | 根据不同的 API 返回不同 |
## 机器人 API 方法
### sendPrivateMsg()
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------- | -------- | ------- | ------------------------------------------------------------ |
| `user_id` | number | - | 对方 QQ 号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
代码
```php
$bot = OneBotV11::get(123456); // 123456是你的机器人QQ
$bot->sendPrivateMsg("627577391", "你好啊!你好你好!");
```
效果
<chat-box :my-chats="[
{type:1,content:'你好啊!你好你好!'}
]"></chat-box>
### sendGroupMsg()
发送群组消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------- | -------- | ------- | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
### sendMsg()
发送消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------------- | -------- | ------- | ------------------------------------------------------------ |
| `message_type` | string | - | 消息类型,支持 `private``group``discuss`,分别对应私聊、群组、讨论组,如不传入,则根据传入的 `*_id` 参数判断 |
| `target_id` | number | - | 目标号码,如 QQ 号,群号,讨论组号 |
| `message` | message | - | 要发送的内容 |
| `auto_escape` | boolean | `false` | 消息内容是否作为纯文本发送(即不解析 CQ 码),只在 `message` 字段是字符串时有效 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------- |
| `message_id` | number (int32) | 消息 ID |
### deleteMsg()
撤回消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------------- | ------ | ------- |
| `message_id` | number (int32) | - | 消息 ID |
响应数据:无
### getMsg()
获取消息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | -------------------------------- |
| `message_id` | number | - | 消息 ID |
响应数据
| 字段名 | 数据类型 | 说明 |
| -------------- | -------------- | ------------------------------------------------------------ |
| `time` | number (int32) | 发送时间 |
| `message_type` | string | 消息类型,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
| `message_id` | number (int32) | 消息 ID |
| `real_id` | number (int32) | 消息真实 ID |
| `sender` | object | 发送人信息,同 [消息事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/message.md) |
| `message` | message | 消息内容 |
### getForwardMsg()
获取合并转发消息。
参数
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ----------- |
| `id` | string | 合并转发 ID |
响应数据
| 字段名 | 类型 | 说明 |
| --------- | ------- | ------------------------------------------------------------ |
| `message` | message | 消息内容,使用 [消息的数组格式](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/array.md) 表示,数组中的消息段全部为 [`node` 消息段](https://github.com/howmanybots/onebot/blob/master/v11/specs/message/segment.md#合并转发自定义节点) |
### sendLike()
发送好友赞。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | -------------------------------- |
| `user_id` | number | - | 对方 QQ 号 |
| `times` | number | 1 | 赞的次数,每个好友每天最多 10 次 |
响应数据
### setGroupKick()
群组踢人。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------------------- | -------- | ------- | ------------------ |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要踢的 QQ 号 |
| `reject_add_request` | boolean | `false` | 拒绝此人的加群请求 |
响应数据:无
### setGroupBan()
群组单人禁言。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | --------- | -------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要禁言的 QQ 号 |
| `duration` | number | `30 * 60` | 禁言时长单位秒0 表示取消禁言 |
响应数据:无
### setGroupAnonymousBan()
群组匿名用户禁言。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------------- | ---------------- | --------- | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `anonymous_or_flag` | object 或 string | - | 要禁言的匿名用户对象(群消息上报的 `anonymous` 字段)或用户的 flag |
| `duration` | number | `30 * 60` | 禁言时长,单位秒,无法取消匿名用户禁言 |
上面的 `anonymous_or_flag` 两者任选其一传入即可。
响应数据:无
### setGroupWholeBan()
群组全员禁言
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | -------- |
| `group_id` | number | - | 群号 |
| `enable` | boolean | `true` | 是否禁言 |
响应数据:无
### setGroupAdmin()
群组设置管理员
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置管理员的 QQ 号 |
| `enable` | boolean | `true` | true 为设置false 为取消 |
响应数据:无
### setGroupAnonymous()
群组匿名
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---------------- |
| `group_id` | number | - | 群号 |
| `enable` | boolean | `true` | 是否允许匿名聊天 |
响应数据:无
### setGroupCard()
设置群名片(群备注)
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---------------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置的 QQ 号 |
| `card` | string | 空 | 群名片内容,不填或空字符串表示删除群名片 |
响应数据:无
### setGroupName()
设置群名。
参数
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------ |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 新群名 |
响应数据:无
### setGroupLeave()
退出群组
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------- | ------- | -------------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `is_dismiss` | boolean | `false` | 是否解散,如果登录号是群主,则仅在此项为 true 时能够解散 |
响应数据:无
### setGroupSpecialTitle()
设置群组专属头衔
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------------- | -------- | ------ | ------------------------------------------------------------ |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | 要设置的 QQ 号 |
| `special_title` | string | 空 | 专属头衔,不填或空字符串表示删除专属头衔 |
| `duration` | number | `-1` | 专属头衔有效期,单位秒,-1 表示永久,不过此项似乎没有效果,可能是只有某些特殊的时间长度有效,有待测试 |
响应数据:无
### setFriendAddRequest()
处理加好友请求
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| --------- | -------- | ------ | ----------------------------------------- |
| `flag` | string | - | 加好友请求的 flag需从上报的数据中获得 |
| `approve` | boolean | `true` | 是否同意请求 |
| `remark` | string | 空 | 添加后的好友备注(仅在同意时有效) |
响应数据:无
### setGroupAddRequest()
处理加群请求 / 邀请
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ------------------------------------------------------------ |
| `flag` | string | - | 加群请求的 flag需从上报的数据中获得 |
| `sub_type` | string | - | `add``invite`,请求类型(需要和上报消息中的 `sub_type` 字段相符) |
| `approve` | boolean | `true` | 是否同意请求/邀请 |
| `reason` | string | 空 | 拒绝理由(仅在拒绝时有效) |
响应数据无
### getLoginInfo()
获取登录号信息
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | QQ 昵称 |
### getStrangerInfo()
获取陌生人信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `user_id` | number | - | QQ 号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------------------------------------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `sex` | string | 性别,`male``female``unknown` |
| `age` | number (int32) | 年龄 |
### getFriendList()
获取好友列表
参数:无
响应数据
响应内容为 JSON 数组,每个元素如下:
| 字段名 | 数据类型 | 说明 |
| ---------- | -------------- | ------ |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `remark` | string | 备注名 |
### getGroupInfo()
获取群信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------ | -------------- | -------------------- |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 群名称 |
| `member_count` | number (int32) | 成员数 |
| `max_member_count` | number (int32) | 最大成员数(群容量) |
### getGroupList()
获取群列表
参数:无
响应数据
响应内容为 JSON 数组,每个元素如下:
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ------ |
| `group_id` | number (int64) | 群号 |
| `group_name` | string | 群名称 |
### getGroupMemberInfo()
获取群成员信息
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------- | ---------------------------------------------------- |
| `group_id` | number | - | 群号 |
| `user_id` | number | - | QQ 号 |
| `no_cache` | boolean | `false` | 是否不使用缓存(使用缓存可能更新不及时,但响应更快) |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------- | -------------- | ------------------------------------- |
| `group_id` | number (int64) | 群号 |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `card` | string | 群名片/备注 |
| `sex` | string | 性别,`male``female``unknown` |
| `age` | number (int32) | 年龄 |
| `area` | string | 地区 |
| `join_time` | number (int32) | 加群时间戳 |
| `last_sent_time` | number (int32) | 最后发言时间戳 |
| `level` | string | 成员等级 |
| `role` | string | 角色,`owner``admin``member` |
| `unfriendly` | boolean | 是否不良记录成员 |
| `title` | string | 专属头衔 |
| `title_expire_time` | number (int32) | 专属头衔过期时间戳 |
| `card_changeable` | boolean | 是否允许修改群名片 |
### getGroupMemberList()
获取群成员列表
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------- | ------ | ---- |
| `group_id` | number | - | 群号 |
响应数据
响应内容为 JSON 数组,每个元素的内容和上面的 `/get_group_member_info` 接口相同,但对于同一个群组的同一个成员,获取列表时和获取单独的成员信息时,某些字段可能有所不同,例如 `area``title` 等字段在获取列表时无法获得,具体应以单独的成员信息为准。
### getGroupHonorInfo()
获取群荣誉信息。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ---------- | -------------- | ------ | ------------------------------------------------------------ |
| `group_id` | number (int64) | - | 群号 |
| `type` | string | - | 要获取的群荣誉类型,可传入 `talkative` `performer` `legend` `strong_newbie` `emotion` 以分别获取单个类型的群荣誉数据,或传入 `all` 获取所有数据 |
响应数据
| 字段名 | 数据类型 | 说明 |
| -------------------- | -------------- | ---------------------------------------------------------- |
| `group_id` | number (int64) | 群号 |
| `current_talkative` | object | 当前龙王,仅 `type``talkative``all` 时有数据 |
| `talkative_list` | array | 历史龙王,仅 `type``talkative``all` 时有数据 |
| `performer_list` | array | 群聊之火,仅 `type``performer``all` 时有数据 |
| `legend_list` | array | 群聊炽焰,仅 `type``legend``all` 时有数据 |
| `strong_newbie_list` | array | 冒尖小春笋,仅 `type``strong_newbie``all` 时有数据 |
| `emotion_list` | array | 快乐之源,仅 `type``emotion``all` 时有数据 |
其中 `current_talkative` 字段的内容如下:
| 字段名 | 数据类型 | 说明 |
| ----------- | -------------- | -------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `avatar` | string | 头像 URL |
| `day_count` | number (int32) | 持续天数 |
其它各 `*_list` 的每个元素是一个 JSON 对象,内容如下:
| 字段名 | 数据类型 | 说明 |
| ------------- | -------------- | -------- |
| `user_id` | number (int64) | QQ 号 |
| `nickname` | string | 昵称 |
| `avatar` | string | 头像 URL |
| `description` | string | 荣誉描述 |
### getCookies()
获取 Cookies。
::: warning 注意
目前开源的 mirai 为底层的机器人客户端均不支持获取 Cookies 和 CSRF Token包括 go-cqhttp。
:::
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------- | -------- | ------ | ----------------------- |
| `domain` | string | 空 | 需要获取 cookies 的域名 |
响应数据
| 字段名 | 数据类型 | 说明 |
| --------- | -------- | ------- |
| `cookies` | string | Cookies |
### getCsrfToken()
获取 CSRF Token
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ------- | -------------- | ---------- |
| `token` | number (int32) | CSRF Token |
### getCredentials()
获取 QQ 相关接口凭证,即上面两个合并。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| -------- | -------- | ------ | ----------------------- |
| `domain` | string | 空 | 需要获取 cookies 的域名 |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------ | -------------- | ---------- |
| `cookies` | string | Cookies |
| `csrf_token` | number (int32) | CSRF Token |
### getRecord()
获取语音。其实并不是真的获取语音,而是转换语音到指定的格式。
> **提示**:要使用此接口,通常需要安装 ffmpeg请参考 OneBot 实现的相关说明。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------------ | -------- | ------ | ------------------------------------------------------------ |
| `file` | string | - | 收到的语音文件名CQ 码的 `file` 参数),如 `0B38145AA44505000B38145AA4450500.silk` |
| `out_format` | string | - | 要转换到的格式,目前支持 `mp3``amr``wma``m4a``spx``ogg``wav``flac` |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------------------------------------------------------------ |
| `file` | string | 转换后的语音文件路径,如 `/home/somebody/cqhttp/data/record/0B38145AA44505000B38145AA4450500.mp3` |
### getImage()
获取图片。
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------ | -------- | ------ | ------------------------------------------------------------ |
| `file` | string | - | 收到的图片文件名CQ 码的 `file` 参数),如 `6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------------------------------------------------------------ |
| `file` | string | 下载后的图片文件路径,如 `/home/somebody/cqhttp/data/image/6B4DE3DFD1BD271E3297859D41C530F5.jpg` |
### canSendImage()
检查是否可以发送图片。
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| ------ | -------- | ------ |
| `yes` | boolean | 是或否 |
### canSendRecord()
检查是否可以发送语音,返回同上。
### getStatus()
获取插件运行状态。
参数:无
响应数据
| 字段名 | 数据类型 | 说明 |
| -------- | -------- | -------------------------------------------------------- |
| `online` | boolean | 当前 QQ 在线,`null` 表示无法查询到在线状态 |
| `good` | boolean | 状态符合预期,意味着各模块正常运行、功能正常,且 QQ 在线 |
| ...... | - | OneBot 实例自行添加的其他内容 |
通常情况下建议只使用 `online``good` 这两个字段来判断运行状态,因为根据 OneBot 实现的不同,其它字段可能完全不同。
### getVersionInfo()
获取版本信息
响应数据
| 字段名 | 数据类型 | 说明 |
| ------------------ | -------- | ----------------------------- |
| `app_name` | string | 应用标识,如 `mirai-native` |
| `app_version` | string | 应用版本,如 `1.2.3` |
| `protocol_version` | string | OneBot 标准版本,如 `v11` |
| …… | - | OneBot 实现自行添加的其它内容 |
### setRestartPlugin()
重启 OneBot 客户端
由于重启 OneBot 实现同时需要重启 API 服务,这意味着当前的 API 请求会被中断,因此需要异步地重启,接口返回的 `status``async`
参数
| 字段名 | 数据类型 | 默认值 | 说明 |
| ------- | -------- | ------ | ------------------------------------------------------------ |
| `delay` | number | `0` | 要延迟的毫秒数,如果默认情况下无法重启,可以尝试设置延迟为 2000 左右 |
响应数据:无
### cleanCache()
清理 OneBot 客户端的缓存。
参数:无
响应数据:无
### 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。
使用不同 OneBot 客户端时,可能有一些 API 不在上方的 OneBot 标准里,这时可以使用此方法进行额外调用。
参数
| 字段名 | 数据类型 | 默认值 |
| -------- | -------- | ------ |
| `action` | string | 必填 |
| `params` | array | `[]` |
例子
```php
$result = $bot->callExtendedAPI("get_group_root_files", ["group_id" => 123456]);
//这里以 go-cqhttp 扩展的一个获取群文件的 API 为例
var_dump($result["data"]);
// 输出群文件列表
```

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

View File

@@ -0,0 +1,179 @@
# Console 控制台
Console 类所在命名空间:`\ZM\Console\Console`
Console 类为框架的终端输出管理类。
## 设置 Log 输出等级
**输出等级** 控制了输出到命令行的内容的重要性。在框架的输出中,消息有以下几种不同等级的类别
- **error** / **log**: 0
- **warning**: 1
- **info** / **success**: 2
- **verbose**: 3
- **debug**: 4
输出等级设置后显示的消息类别为小于等于当前 log 的。假设你将 log 等级设置为 3你可以看到除 debug 外的所有 log 内容。
通过配置文件 `global.php` 中的 `init_atomics -> info_level` 的数值你可以更改框架的默认 log 等级(默认为 2
你也可以在启动框架的命令行中添加参数来切换 log 等级:
```bash
vendor/bin/start server --log-error # 以 error 等级启动框架
vendor/bin/start server --log-warning # 以 warning 等级启动框架
vendor/bin/start server --log-info # 以 info 等级启动框架
vendor/bin/start server --log-verbose # 以 verbose 等级启动框架
vendor/bin/start server --log-debug # 以 debug 等级启动框架
```
## 使用 Log 输出内容
作为模块开发者的你,你可以主动调用框架内的 Console 类输出信息到终端。
### Console::log()
输出 0 级别的普通 log。
- 参数:`$msg, $color`,分别为内容和字体颜色。
> 此 log 不会被 info_level 所限制,无论如何也会输出到终端。
### Console::error()
输出 error 级别的红色醒目 log。一般此 log 为框架内部出现不可忍受的错误比如内存不足、PHP fatal error 等错误。
- 参数:`$msg`
> 此 log 不会被 info_level 所限制,无论如何也会输出到终端。
### Console::warning()
输出 warning 级别的 log。
::: warning 注意
框架内出现的用户态异常,比如无法发送 API、无法连接数据库等错误都是 warning 错误,不会导致框架崩溃或功能错误的异常情况建议都使用 warning 输出而不是 error。
:::
### Console::info()
输出 info 级别的 log。
### Console::success()
输出 success 级别的log。
### Console::verbose()
输出 verbose 级别的 log。
### Console::debug()
输出 debug 级别的 log。
### Console::stackTrace()
输出栈追踪信息。
### Console::setColor()
返回:彩色的字符串。
- **string**: 要变颜色的字符串
- **color**: 要变的颜色。支持 `red``green``yellow``reset``blue``gray``gold``pink``lightblue``lightlightblue`
```php
Console::log("This is normal msg. (0)");
Console::error("This is error msg. (0)");
Console::warning("This is warning msg. (1)");
Console::info("This is info msg. (2)");
Console::success("This is success msg. (2)");
Console::verbose("This is verbose msg. (3)");
Console::debug("This is debug msg. (4)");
Console::stackTrace();
$str = Console::setColor("I am gold color.", "gold");
```
## 终端交互命令
炸毛框架支持从终端输入命令来进行一些操作,例如重启框架、停止框架、执行函数等。
::: warning 注意
在 Docker、systemd、daemon 状态下启动的框架会自动关闭终端等待输入,交互不可用。
:::
### reload
重新加载除 `src/Framework/` 下的所有模块。
- 别名:`r`
### stop
停止框架。
### logtest
输出各种等级的 log 示例文本。
### call
执行对应类的成员方法。下面是例子:
```bash
call \ZM\Utils\ZMUtil reload
```
### bc
直接执行 PHP 代码,输入格式为 base64。
```bash
bc XEZyYW1ld29ya1xDb25zb2xlOjp3YXJuaW5nKCJoZWxsbyB3YXJuaW5nISIpOw==
# 代码内容:\ZM\Console\Console::warning("hello warning!");
# 终端输出:[19:14:32] [W] hello warning!
```
### echo
输出文本
```bash
echo hello
```
### color
按照颜色输出文本
```bash
color green 我是绿色的字
```
## MOTD
在 1.4 版本开始,框架支持启动时的 motd 内容修改。
文件位置:`config/motd.txt`
其中,默认的 `Zhamao` 字样的 MOTD 是使用 **figlet** 命令生成的,`figlet "Zhamao"`,你也可以针对自己的机器人名称或品牌进行生成。
## 设置输出主题
Console 组件支持为多种不同的终端设置不同的主题,比如有些人喜欢使用白色的终端,但是白色终端下 info 的颜色很浅,看不到,还有人使用不能显示颜色的黑白终端.....
```bash
vendor/bin/start server --log-theme={主题名}
```
现有支持的主题有:`default``white-term``no-color`
```bash
vendor/bin/start server --log-theme=white-term # 如果用的是白色终端,这个主题更友好
vendor/bin/start server --log-theme=no-color # 如果不想让 log 带有任何颜色,使用无色主题
```

View File

@@ -0,0 +1,428 @@
# 上下文
上下文作为整个框架中最重要的内容之一,请务必理解和完整地阅读此部分!
一个上下文描述了一个事件和所关联的对象的环境。例如:你在处理 HTTP 请求的 `@RequestMapping` 绑定的事件中,你需要获取请求的 HTTP 头和 Cookie再比如你在处理 QQ 机器人发来的命令 `@CQCommand("随机数")` 的时候,在这个方法内,你需要获取发来的人的 QQ 号码。以上我们将处理以上运行环境的对象叫做上下文。
由于 Swoole 的协程加持,我们利用了协程 ID 绑定对象来进行构造上下文。
以默认的机器人收发消息为例,通过对默认模块的了解,我们可以知道,在绑定 `@CQCommand` 等类似事件后,你可以用上下文获取发来这条消息的人的 QQ 号码:
```php
/**
* @CQCommand("你好")
*/
public function hello() {
$user_id = ctx()->getUserId();
ctx()->reply("你好啊,".$user_id.",很高兴认识你!");
}
```
`context()` 就是获取上下文对象的全局函数,它还有简写:`ctx()`
当然,上下文中的方法不是每个都能在任何时候使用的。例如 `getUserId()` 你不能在 `@RequestMapping` 注解的函数中使用,因为它不是机器人消息的上下文。下面说明上下文对象的方法中,每个都会说明每个方法可以在哪些事件中使用:
## getServer() - 获取 Server 对象
获取 Swoole WebSocker Server 对象。此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server)。
可以使用的事件:`@OnMessageEvent()``@OnOpenEvent()``@OnCloseEvent()``@OnStart()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
## getFrame() - 获取 WS 数据帧
获取 `\Swoole\Websocket\Frame` 对象,此对象是 Swoole 的对象,详情见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)。
可以使用的事件:`@OnMessageEvent()` 以及所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等,
## getFd() - 返回 fd 值
获取当前连入 Swoole 服务器的连接文件描述符 ID。返回 int。一般代表连接号可用来绑定对应链接。
可以使用的事件:所有 **getFrame()** 可以使用的,`@OnOpenEvent()``@OnCloseEvent()`
::: tip 提示
值得注意的是,由于机器人客户端和炸毛框架的连接是通过 WebSocket 进行的,而 WebSocket 是长连接,所以同一个机器人一次连接下收发消息所用的连接是同一个,所以 Fd 也是相同的。同理,炸毛框架的内部来区分多个机器人也是通过这一 Fd 进行判定的。
:::
代码
```php
/**
* @CQCommand("测试fd")
*/
public function testfd() {
ctx()->reply("当前机器人连接的fd是".ctx()->getFd()."机器人QQ是".ctx()->getRobotId());
}
```
效果
<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() - 获取事件完整数据
返回 CQHTTP 事件上报的原始数据包,已经被解析成数组,可以直接操作。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
```php
/**
* @CQMessage(user_id=123456)
*/
public function onMessage() {
$data = ctx()->getData();
ctx()->reply("消息类型是:" . $data["message_type"]);
}
```
<chat-box :my-chats="[
{type:2,content:'假设我是QQ为123456的用户私聊发消息'},
{type:0,content:'哈咯!!'},
{type:1,content:'消息类型是private'},
]"></chat-box>
## getRequest() - HTTP 请求对象
返回 `\Swoole\Http\Request` 对象,可在 `@RequestMapping` 中使用,获取 Cookie请求头GET 参数什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httprequest)。
可以使用的事件:`@RequestMapping()``@OnRequestEvent()``@OnOpenEvent()`
## getResponse() - HTTP 响应对象
返回 `\Swoole\Http\Response` 对象的增强版,可在 HTTP 请求相关的事件中使用,返回内容和设置 Cookie 什么的。[Swoole 文档](https://wiki.swoole.com/#/http_server?id=httpresponse)。
可以使用的事件:`@RequestMapping()``@OnRequestEvent()`
下面是使用以上两个功能的组合示例:
```php
/**
* @RequestMapping("/ping")
*/
public function ping() {
$name = ctx()->getRequest()->get["name"] ?? "unknown";
ctx()->getResponse()->end("Hello ".$name."!");
}
```
## getConnection() - WS 连接对象
返回此上下文相关联的 WebSocket 连接对象。详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client)。
可以使用的事件:所有 **getFrame()** 可以使用的都可以使用。
## getCid() - 上下文 ID
返回当前上下文所绑定的协程 ID此 ID 和 `\Co::getCid()` 返回值一样。
## getRobot() - 获取机器人 API 对象
返回当前上下文关联的机器人 API 调用对象 [ZMRobot](../bot/robot-api.md)。
可以使用的事件:所有 HTTP API 发来的事件:`@CQCommand()``@CQMessage()` 等。
```php
ctx()->getRobot()->sendPrivateMsg(123456, "发送私聊消息");
```
<chat-box :my-chats="[
{type:2,content:'正在和机器人聊天'},
{type:1,content:'发送私聊消息'},
]"></chat-box>
## getMessage() - 获取消息
获取 data 数据中的 `message` 消息,用于快速获取用户消息事件的消息内容。
可以使用的事件:`@CQCommand()``@CQMessage``@CQBefore("message")``@CQAfter("message")`
代码
```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 号
获取发消息的用户的 QQ 号码。
可以使用的事件:所有 **含有** `user_id` 上报参数的 OneBot 事件。
```php
/**
* @CQCommand("whoami")
*/
public function whoami() {
ctx()->reply("你是".ctx()->getUserId()); //返回你是123456
}
```
## getGroupId() - 获取 QQ 群号
获取发消息来自的 QQ 群号。
可以使用的事件:所有含有 `group_id` 上报参数的 OneBot 事件。
## getMessageType() - 消息类型
获取消息类型,同参数 `message_type`
可以使用的事件:所有 `post_type``message` 的响应事件,如 `@CQMessage``@CQCommand`
## getRobotId() - 机器人 QQ 号
获取事件上报的机器人自己的 QQ 号码。
可以使用的事件:所有 OneBot 发来的事件:`@CQCommand()``@CQNotice()` 等。
## setMessage() - 设置消息
`getMessage()` 对应,用于更改上下文中保存的事件信息,可以用于消息变更和过滤。
## setUserId() - 设置用户 ID
与上同理,更改 `user_id`
## setGroupId() - 设置群号
与上同理。
## setMessageType() - 设置类型
与上同理,修改消息类型。
## setData() - 设置数据包
与上同理,与 `getData()` 对应,用于更改上下文中的 `data`
## getCache() - 上下文缓存
获取保存在上下文中的临时缓存变量。当相关联的事件结束后,数据会从内存中被释放。用于同一事件的多个函数中的信息传递。
- 参数:`$key`,缓存变量的键名
- 返回:`mixed`,存入缓存的变量值。
```php
$a = ctx()->getCache("block_continue");
// 如果变量不存在,则返回 null
```
## setCache() - 上下文缓存
`getCache()` 对应,是设置内容的。
```php
ctx()->setCache("abc", "asdasd");
$result = ctx()->getCache("abc"); // asdasd
```
## reply() - 快速回复
快速回复当前用户消息内容。
- 参数1`$msg`,字符串,你要回复的消息内容
- 参数2`$yield = false`,可选,当为 `true` 时,会协程等待后返回 **消息回复** 的结果,包括 API 状态码、消息 `message_id` 等。
```php
$r = ctx()->reply("我又好了。");
if($r["retcode"] == 0) Console::success("消息发送成功!");
```
## finalReply() - 快速回复
快速回复用户消息,并阻止其他模块接下来继续处理此事件。
参数同 `reply()`
## waitMessage() - 等待用户消息
- 参数:`waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "")`
- 用途:等待用户输入消息
`$prompt` 参数为回复用户的文本内容,`$timeout` 是等待用户回复的超时时间(秒)`$timeout_prompt` 是超时后回复用户的文本。
这个功能可以让开发机器人的代码逻辑和实际贴合,避免回调地狱、拼接参数、上下文脱节等问题,比如下方的示例,可以仅仅用两行代码实现一个问答式的对话过程。
用法示例:
```php
/**
* @CQCommand("自我介绍")
*/
function yourName(){
$r = ctx()->waitMessage("你叫啥名字呀?", 600, "你都10分钟不理我了嘤嘤嘤");
ctx()->finalReply("好的,可爱的机器人记住你叫 ".$r." 啦!以后多聊天哦!");
}
```
<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() - 自动获取参数
`waitMessage()` 的封装,目的是让机器人的回复更加智能化。最好的例子就是在框架自带的默认示例中“随机数”的例子,我们假设要写一个随机数功能,但是用户从来都是不思考就使用机器人的。抛开人工智能,我们能做的就是“专家系统”,同时让我们写的代码尽可能适配用户所说的每一句话:
- 随机数 1 100
- 随机数(一般不知道怎么用这个功能的人都会只说一个关键词)
- 从2到9的随机数
所以,在匹配第一和第二种情况时候,我们不需要重复写代码,而第一种的话用户已经将参数给你的时候,你不需要再次使用 `waitMessage()` 方式进行等待询问,只需要取到使用就好了。`getArgs()` 就是做这个的。
定义:`getArgs($mode, $prompt_msg)`
`$mode`:获取模式,有三种:
- `ZM_MATCH_ALL`:效果等同于 `getFullArg()`,获取全部的内容,把空格也当作一部分
- `ZM_MATCH_NUMBER`:效果等同于 `getNumArg()`,获取下一个数字参数
- `ZM_MATCH_FIRST`:效果等同于 `getNextArg()`,获取下一个参数
`$prompt_msg`:字符串,指定如果参数缺失时询问用户的内容。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getArgs(ZM_MATCH_FIRST, "请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<chat-box :my-chats="[
{type:0,content:'test'},
{type:1,content:'请输入你要传入的参数内容'},
{type:0,content:'test2'},
{type:1,content:'参数内容test2'},
]"></chat-box>
`getArgs()` 也有三层封装,在使用过程中避免麻烦的话,推荐使用下面这几种 `get*Arg()` 方式。
## getFullArg()
获取关键词后的整个字符串参数,包括空格,如果不存在则询问。
典型例子:`复读机 你好 你好`,获取参数时会将 `你好 你好` 当作一个参数来获取。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getFullArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<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()
获取下一个参数分隔符可以是空格tab。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的参数内容");
return "参数内容:".$s;
}
```
<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()
> 2.1.5 版本起可用。
获取下一个数字型参数,如果 `is_numeric()` 为 true 则获取成功,如果没有符合的则询问用户。
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
$s = ctx()->getNextArg("请输入你要传入的数字内容");
return "数字参数内容:".$s;
}
```
<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()
获取整个上下文的所有内容的数组形式。
```php
$arr = ctx()->copy();
dump($arr);
```
## getOption() - 获取匹配参数内容
```php
/**
* @CQCommand("test")
*/
public function argTest1() {
return "参数内容:".implode(", ", ctx()->getOption());
}
```
<chat-box :my-chats="[
{type:0,content:'test abc 334 argtest'},
{type:1,content:'参数内容abc, 334, argtest'},
]"></chat-box>

View File

@@ -0,0 +1,62 @@
# 协程池
首先要声明的一点是,协程池这个概念是我自己编的。
因为协程的特点,它是单线程下运行的,所以在一个进程内同时实际上只会有一个协程的代码在执行逻辑,但是后面的 IO 操作、协程挂起等待的操作都是同时去做的,比如数据库的大数据读取、写入需要耗时几秒甚至几十秒,这时用基于协程的 MySQL 连接池就完全不是问题。
但是就拿 MySQL 举例,我们 MySQL 使用的是 TCP 连接,而无论是 MySQL 还是 TCP 连接,最大数量都是有限的。我们即使设置了允许最大协程数量非常大,比如上百万,但是也不能让数据库连接池一个池支持上百万的连接。
这时假设高并发进来了怎么办呢?这时就需要框架提出的一个折中方案:协程池了。
顾名思义,协程池是一个容纳协程的区域,而协程里又容纳着各种各样需要阻塞调用被协程调用的 IO 操作,协程池用作限制协程的数量。
```php
use ZM\Utils\CoroutinePool;
use ZM\DB\DB;
// 传统写法,一旦高并发则可能导致 Too many connections
go(funuction(){
DB::rawQuery("INSERT INTO users VALUES(?,?)", ["admin", "password"]);
});
// 协程池写法
CoroutinePool::go(function(){
DB::rawQuery("INSERT INTO users VALUES(?,?)", ["admin", "password"]);
}, "foo");
```
参数:`go(callable $func, $name = "default")`
`$name` 为协程池对应的名字,你可以设置多个协程池,用来支持不同的需要限制并发 IO 数量的地方,例如 Redis 和 MySQL 设置不同的名字。`$func` 可为闭包或可调用的方法名称或数组。
## 配置
默认情况下,直接调用 `CoroutinePool::go()` 时,协程池大小为 30也就是如果有 30 个协程进入了挂起状态(比如数据库在执行查询语句),那么更多的协程执行时就会阻塞并以协程等待的方式等待,直到现有的 30 个协程中的一部分完成了它的工作。
## 方法
### CoroutinePool::go()
将协程放入协程池运行。
如果不写 `$name` 参数,则使用的是默认协程池。
### CoroutinePool::defaultSize()
设置默认协程池的大小(默认 30
```php
CoroutinePool::defaultSize(64);
for($i = 0; $i < 1000; ++$i) {
CoroutinePool::go(function(){
DB::rawQuery("SELECT * FROM users");
});
}
```
### CoroutinePool::setSize()
定义:`setSize($name, int $size)`
`$name` 为字符串,是你要用的协程池的名称。
`$size` 为大小,最大不可超过 Swoole 配置文件中指定的最大协程数量。

View File

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

View File

@@ -0,0 +1,323 @@
# 全局方法
全局方法就是 PHP 的全局函数,任意位置都可以调用,无需使用 use 字样。
## getClassPath()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L24)
根据加载的用户编写的代码类名来获取类所在的文件路径。
**src/Module/Example/Hello.php**
```php
<?php
namespace Module\Example;
class Hello { ... }
```
**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));
}
}
```
**输出结果**
```
[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)`
```php
$s = explodeMsg("你好啊 你好你好\n我还有多个空格 哈哈哈");
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
echo unicode_decode("\u4f60\u597d"); // 你好
```
## matchPattern()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L91)
根据星号匹配字符串(非正则表达式)。
匹配示例:
- `你今天*了吗` -> 你今天喝水了吗
- `*的天气怎么样` -> 德州的天气怎么样
- `把*翻译成*` -> 把茶翻译成英语
定义:`matchPattern($pattern, $context)`
`$pattern` 为匹配模式,例如 `你今天*了吗`
`$context` 为要判断是否匹配的内容。
返回值:`bool`,当为 true 时代表规则是匹配的false 代表不匹配。
```php
matchPattern("*是个啥?", "996是个啥"); // true
matchPattern("我想听*唱歌", "你想听谁唱歌"); // false
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)`
```php
split_explode(" ", "前进20 急啊急啊"); // ["前进","20","急啊急啊"]
```
`$del``explode()` 的第一个参数作用相同,作为初期分割的标志。
`$str` 表示待分割的内容。
`$divide_en` 表示是否分割中文和英文,如果为是,则中文和英文之间也会被分割开。
## matchArgs()
[源码](https://github.com/zhamao-robot/zhamao-framework/blob/master/src/ZM/global_functions.php#L135)
`matchPattern()` 的扩展,如果 `matchPattern()` 格式的字符串和模式匹配成功,则通过星号位置来提取星号匹配到的内容,参数同 `matchPattern()`
```php
$r = matchArgs("把*翻译成*", "把日语翻译成英语"); // ["日语","英语"]
```
## connectIsQQ()
判断当前 WebSocket 连接是否为 OneBot 标准的机器人客户端。
## connectIsDefault()
判断连接是否是未定义类型的 WebSocket 连接。
## connectIs()
判断连接是否是对应类型的 WebSocket 连接。
```php
connectIs("your_another_type_connect");
```
## set_coroutine_params()
设置当前上下文中的一些变量。
```php
set_coroutine_params(["data" => [
"post_type" => "message",
...
]]);
```
## ctx()
别名:`context()`,获取当前协程的上下文,见 [上下文](/component/context/)。
## zm_sleep()
协程版 `sleep()` 函数。
定义:`zm_sleep($s = 1)`
`$s`睡眠的时间可支持小数。例如0.001 代表 1 毫秒)
为什么不用 PHP 自带的 sleep 呢?因为炸毛框架是基于协程的,协程版 sleep 需要使用 Swoole 自带的 sleep。此函数做了一个简单的封装。
```php
zm_sleep(5);
zm_sleep(0.05);
```
## zm_exec()
执行系统命令,替代 PHP 的 `exec()`
定义:`zm_exec($cmd)`
返回值:
```php
array(
'code' => 0, // 进程退出的状态码
'signal' => 0, // 信号
'output' => 'hello world', // 输出内容
);
```
```php
$result = zm_exec("echo 'hello world'")["output"];
```
## zm_cid()
获取当前协程的 ID效果等同于 `\Swoole\Coroutine::getCid()`
## zm_yield()
挂起当前协程,直到手动恢复,效果等同于 `\Swoole\Coroutine::yield()`
## zm_resume()
恢复继续执行协程,效果等同于 `\Swoole\Coroutine::resume()`
```php
$r = 0;
function test() {
echo "hello-1\n";
global $r;
$r = zm_cid();
zm_yield();
echo "hello-2\n";
}
go("test");
echo "hello-3\n";
zm_resume($r);
```
输出结果:
```
hello-1
hello-3
hello-2
```
## server()
获取 Swoole Server 对象进行操作,效果等同于 `\ZM\Framework::$server`
```php
echo server()->worker_id.PHP_EOL; // 0
```
## bot()
返回 ZMRobot 操作机器人 API 的对象。
对于默认的模式,如果框架连接了多个机器人实例,则会随机返回一个机器人的 API 实例。如果使用了单例模式,则返回单例模式的机器人 API 实例。
```php
bot()->sendPrivateMsg(123456, "你好啊!!");
// 等同于 ZMRobot::getRandom()->sendPrivateMsg(123456, "你好啊!!");
```
## zm_atomic()
获取计时器,效果同 `\ZM\Store\ZMAtomic::get($name)`
定义:`zm_atmoic($name)`
## uuidgen()
> 2.2.5 版本起可用。
生成一个随机的 uuid支持大写或小写。
定义:`uuidgen($uppercase = false)`
`$uppercase``true` 时,返回的 uuid 中字母都是大写。
## working_dir()
> 2.2.6 版本起可用。
获取框架运行的工作目录。例如你是从 `/root/framework-starter/` 目录启动的框架,`vendor/bin/start server`,那么 `working_dir()` 返回的就是 `/root/framework-starter`。(注意,返回的目录最后没有斜杠,请自行添加。)
## getAllFdByConnectType()
获取同类型的所有连接的描述符 ID。
定义:`getAllFdByConnectType(string $type = 'default'): array`
`$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);
```
![](https://static.zhamao.me/images/docs/ba026ca11332b1a4ad68a549165230e6.png)
## 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)`

View 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 连接,效果如下:
本地主机:
![img.png](https://static.zhamao.me/images/docs/3432551c08b34ca10aaf19f3f82aedeb.png)
从别的主机:
![img.png](https://static.zhamao.me/images/docs/6f35f2745d66c7e186da75b6f09248c2.png)
如果将 `host` 改为 `0.0.0.0` 或对应监听地址,即可指向性访问。
但是,如果你又想远程连接,又想保证安全,那么可以设置一个 token 参数,来保证连接时需要输入 token 才能使用远程终端。
假设我们的 token 是 `iAMTokEn`
![img.png](https://static.zhamao.me/images/docs/e502af4c0fd9359615548303cacb70dd.png)
## 使用
默认情况下,使用 `nc` 命令即可。
```bash
nc <your-host> <your-port> -vvv
# nc 127.0.0.1 20002 -vvv
```
输入 help 即可查看内置的常用指令:
![img.png](https://static.zhamao.me/images/docs/7b74aa2b487c86482097ec7692c66e08.png)

View File

@@ -0,0 +1,43 @@
# 单例类 - SingletonTrait
单例类,顾名思义,就是让用户声明的类拥有单例的特性,而这一组件引入的方式也最直接。它是一个 PHP 的 `trait`
我们传统写单例类的方式很手动,比如下面这样:
```php
<?php
class Foo {
public $test = 0;
private static $instance;
public static function getInstance() {
if (null === self::$instance) {
self::$instance = new Foo();
}
return self::$instance;
}
}
Foo::getInstance()->test = 4;
$obj = Foo::getInstance()->test;
var_dump($obj); // 4
```
这就要求我们每个需要声明为单例的类都写一个成员静态方法和成员静态变量。
框架使用了 PHP Trait 来快速让一个类支持这一特性:
```php
<?php
use ZM\Utils\SingletonTrait;
class Foo {
use SingletonTrait;
public $test = 0;
}
Foo::getInstance()->test = 5;
var_dump(Foo::getInstance()->test);
```
只需要在类中使用:`use \ZM\Utils\SingletonTrait;` 一句话即可。

View 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");
```

View 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)。

View 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)。

View File

@@ -0,0 +1,274 @@
# ZMRequestHTTP 客户端)
框架提供了轻量的 HTTP 请求发起工具类,直接静态调用即可。
命名空间:`use ZM\Requests\ZMRequest;`
::: warning 注意
在使用 Swoole 4.6.0 以下(不包含)的版本时,最好使用 Swoole 官方推荐的 Saber 或者 ZMRequest 这个轻量的 HTTP 请求客户端,不要使用 curl_exec因为在老版本的 Swoole 上对 curl 的协程 Hook 支持不是很完善。
:::
## ZMRequest::get()
发起 GET 请求。
定义:`ZMRequest::get($url, $headers = [], $set = [], $return_body = true)`
全局函数别名:`zm_request_get($url, $headers = [], $set = [], $return_body = true)`
`$url`:要请求的 url`http://captive.apple.com/`
`$headers`:要请求的 Headers例如`["User-Agent" => "Chrome"]`,数组形式
`$set`:请求时的一些设置,例如超时时间等等。详见下方“设置参数”
`$return_body`:是否只返回请求回来的内容部分,默认为 true如果为 false 时则会返回一个 `\Swoole\Coroutine\Http\Client` 对象,可查阅 [Swoole 文档](http://wiki.swoole.com/#/coroutine_client/http_client) 进行接下来的一系列操作。
如果 `$return_body` 为 true但是请求失败HTTP 状态码不是 200 或无法连接到目标服务器或者无法解析域名等问题)时,方法会返回 false否则会返回内容。
返回值:`false|string|\Swoole\Coroutine\Http\Client`
```php
$r = ZMRequest::get("http://captive.apple.com/", ["User-Agent" => "Chrome"]);
echo $r.PHP_EOL; // <HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>
```
```php
$r = zm_request_get("http://captive.apple.com/", [], [], false);
echo $r->body.PHP_EOL; // 这行输出和上方的一致
dump($r);
/*
^ Swoole\Coroutine\Http\Client {#170
+errCode: 0
+errMsg: ""
+connected: false
+host: "captive.apple.com"
+port: 80
+ssl: false
+setting: array:1 [
"timeout" => 15.0
]
+requestMethod: "GET"
+requestHeaders: []
+requestBody: null
+uploadFiles: null
+downloadFile: null
+downloadOffset: 0
+statusCode: 200
+headers: array:4 [
"content-type" => "text/html"
"content-length" => "68"
"date" => "Thu, 07 Jan 2021 06:22:32 GMT"
"connection" => "keep-alive"
]
+set_cookie_headers: null
+cookies: null
+body: "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>"
}
*/
```
## ZMRequest::post()
发送一个 POST 请求。
定义:`ZMRequest::post($url, array $header, $data, $set = [], $return_body = true)`
全局函数别名:`zm_request_post($url, array $header, $data, $set = [], $return_body = true)`
`$url`:同上,填入 url必填
`$header`:请求的 Headers必填数组形式例如 `["Content-Type" => "application/json"]`
`$data`:请求的数据体,默认应该传入数组,如果传入 `array` 类型,则会默认当作 `Content-Type: application/x-www-form-urlencoded` 方式自动转码和转换,例如 `["key1" => "b1", "key2" => "b2"]` 会变成 `key1=b1&key2=b2`
`$set`:同上,见下面的设置参数部分。
`$return_body`:同上。
```php
$s = ZMRequest::post("http://captive.apple.com/", ["Content-Type" => "application/json"], json_encode(["key1" => "value1"]));
```
## ZMRequest::request()
发起自定义一切参数的 HTTP 请求。
参数:
- `$url`请求的链接自动解析端口、HTTPS、DNS 等操作
- `$attribute`:请求的属性,示例见下方
- `$return_body`:可选参数,`bool` 类型,和上面的 `$return_body` 参数意义相同
其中 `$attribute` 参数对应可设置的有:
- `method`:可选 `GET``POST` 等 HTTP 请求的方式
- `set`:设置 Swoole 客户端的参数
- `headers`:要请求的 HTTP Headers
- `data`:请求的 body 数据,为数组时自动转换头部为 `x-www-form-urlencoded`
- `file`:要发送的文件,数组,可选多个文件
例1使用 GET 请求发送带有 Body 的 HTTP 请求
```php
$r = ZMRequest::request("http://example.com", [
"method" => "GET",
"data" => [
"key1" => "value1"
]
]);
```
例2设置请求超时时间并指定自定义头部
```php
$r = ZMRequest::request("http://example.com", [
"method" => "POST",
"headers" => [
"X-Custom-Header" => "Hello world",
"User-Agent" => "HEICORE"
],
"set" => ["timeout" => 10.0]
]);
```
例3发送文件和 data
```php
$r = ZMRequest::request("http://example.com/sendfile", [
"file" => [
[
"path" => "/path/to/image1.jpg", // path字段必填
"name" => "file1", // name字段必填这个是 POST 过去的 key
//"mime_type" => "image/jpeg", // 可选字段,底层会自动推断
//"filename" => "a.jpg", // 可选字段,文件名称
//"offset" => 0, // 可选字段,可以从指定文件的中间部分开始传输数据,此特性用于断点续传
//"length" => 1024 // 可选字段,默认为整个文件的尺寸
],
[
"path" => "/path/to/image2.jpg",
"name" => "file2"
]
],
"data" => [
"key1" => "value1"
]
]);
```
## ZMRequest::downloadFile()
下载文件到本地。
定义:`ZMRequest::downloadFile($url, $dst = null)`
`$url`:不多讲,下载链接。
`$dst`:本地位置,例如 `/tmp/hello.html`
下载成功返回 true 或指定的文件位置,失败返回 false。
```php
ZMRequest::downloadFile("http://captive.apple.com/", "/tmp/apple.html");
```
## ZMRequest::websocket()
创建一个 WebSocket 连接。因为 Swoole 提供的是同步协程的方案,但对于 WebSocket 这样的全双工通信,反而不是一个好的代码逻辑,炸毛框架将此同步协程的方案封装成了异步事件调用的方式。
定义:`ZMRequest::websocket($url, $set = ['websocket_mask' => true], $header = [])`
返回:一个 `\ZM\Requests\ZMWebSocket` 对象
效果等同于:`$s = new \ZM\Requests\ZMWebSocket($url, $set = ['websocket_mask' => true], $header = [])`
这个是 ZMRequest 扩展而来的异步 WebSocket 客户端,可供方便地连接、收发 WebSocket 消息所定制。
命名空间:`\ZM\Requests\ZMWebSocket`
```php
$ws = ZMRequest::websocket("ws://127.0.0.1:12345/"); //使用工具函数
// $ws = new ZMWebSocket("ws://127.0.0.1:12345/"); //直接构造
if($ws->is_available) {
$ws->onMessage(function(\Swoole\WebSocket\Frame $frame, $client) {
var_dump($frame->data);
});
$ws->onClose(function($client){
Console::info("Websocket closed.");
});
$result = $ws->upgrade();
var_dump($result);
}
```
### 属性
#### is_available
`bool` 类型,用于判断构造对象是否成功或链接是否可用。在构建新的对象并执行 `upgrade()` 前,如果 ws 链接没有问题,则会变为 true`onClose()` 回调执行后,此值变回 false。
### 方法
#### __construct()
客户端对象的构造方法。
参数:
- `$url`:要请求到的 WebSocket 目标地址,必须以 `ws(s)://` 开头
- `$set`可选Swoole 客户端的参数,例如超时、是否使用 `websocket_mask` 等,如果为空数组则默认为 `["websocket_mask" => true]`,具体可设置的内容见 [Swoole 文档](https://wiki.swoole.com/#/coroutine_client/http_client?id=set)
- `$header`:可选,请求的头部信息,数组
```php
$a = new ZMWebSocket("ws://127.0.0.1:8080/", ["websocket_mask" => true], [
"User-Agent" => "Firefox"
]);
```
#### onMessage()
设置收到消息的回调函数。
回调的参数:
- `$frame``Swoole\WebSocket\Frame` 类型,消息帧,一般只用 `$frame->data` 获取数据,具体见 [Swoole 文档](https://wiki.swoole.com/#/websocket_server?id=swoolewebsocketframe)
- `$client``Swoole\Coroutine\Http\Client` 类型,为客户端本身的对象,用于 push 数据等
```php
$a->onMessage(function($frame, $client){
echo "收到消息:".$frame->data.PHP_EOL;
$client->push("hello world");
});
```
#### onClose()
设置连接断开后执行的回调函数。
回调的参数:
- `$client`:同上,但断开连接后不能使用 `push()` 发送数据了,只建议作为重连等机制的使用
```php
$a->onClose(function($client){
echo "WS 链接断开了!".PHP_EOL;
});
```
#### upgrade()
发起连接。
返回值:`true|false`,当为 `true` 时代表握手成功,此时可以在回调里愉快地收发消息了。如果为 `false` 表明握手失败。
::: warning 注意
这里由于是协程转异步,所以不能确定 `upgrade()``onMessage()` 哪个先会被触发(一般情况下如果服务器不是立刻响应回包信息,总是会先返回 `upgrade()` 的结果。
:::
## 设置参数
见:[Swoole - HTTP 客户端](http://wiki.swoole.com/#/coroutine_client/http_client?id=set)

View File

@@ -1,3 +0,0 @@
# 框架组件
还没写到这里,不着急

View 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
```
通过此命令可以查看模块相关的信息,如未打包但已配置的模块信息等。

View File

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

View File

@@ -0,0 +1,67 @@
# ZMAtomic 原子计数器
原子计数器是用于多进程间跨进程使用的原子计数使用的,比如统计入站请求数量等。此功能基于 Swoole 的 Atomic详情见 [Swoole - 文档]([进程间无锁计数器(Atomic) (swoole.com)](http://wiki.swoole.com/#/memory/atomic))。
## 配置和初始化
见配置文件:`config/global.php` 中的 `init_atomics` 字段:
```php
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
'foo' => 0,
'bar' => 4,
];
```
这时我们就成功初始化两个原子计数器,名字分别为 `foo``bar`
::: warning 注意
初始化的值必须是不小于 0 的 int32 值!
:::
## 使用
定义和命名空间:`ZM\Store\ZMAtomic`
连接计数示例:
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnRequestEvent;
use ZM\Store\ZMAtomic;
class Hello {
/**
* @OnRequestEvent()
*/
public function onRequest() {
$cnt = ZMAtomic::get("foo")->add(1);
ctx()->getResponse()->end("当前已访问:".$cnt."");
}
}
```
### ZMAtomic::get()->get()
获取计数的数字:`dump(ZMAtomic::get("bar")->get());` 返回 4。
### ZMAtomic::get()->add($num)
加上一定的数并返回结果:`dump(ZMAtomic::get("bar")->add(5));` 返回 9。
### ZMAtomic::get()->sub($num)
要减少的数值(必须为正整数):`dump(ZMAtomic::get("bar")->sub(5));` 返回 5。
### ZMAtomic::get()->set($num)
设置计数的数字:`ZMAtomic::get("bar")->set(77);`
::: tip 提示
还有一些不常用的方法,可以看 Swoole 官方的文档,这里就不一一列举了。
:::

View 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()` 获取目录后面需要加 `/` 再追加自己的文件名或子目录名。
:::

View File

@@ -0,0 +1,353 @@
# LightCache 轻量缓存
在炸毛框架 1.x 时代,框架里有非常方便使用的 ZMBuf 缓存,但是由于 2.x 版本框架加入了多进程模式所以不能再以传统的存到全局变量的方式来构建和管理缓存了LightCache 就是替代方案。LightCache 依旧是 key-value 键值对形式的存储,支持多种类型的变量。
定义:`ZM\Store\LightCache`
## 与 ZMBuf 的不同
从存储内容角度LightCache 存入的是 Swoole 初始化的共享内存,基于 Swoole/Table 编写。优势在于多进程下的性能极佳,而且没有数据同步问题;劣势在于它需要在启动框架前就声明总大小,不能根据存储数据的大小来划定,需提前指定最大能存储的容量。而 ZMBuf 基于直接把变量存到静态成员中 `public static $data` 类似这样,且 1.x 框架基于单进程单线程,无任何数据同步的问题。
总之来说LightCache 是让用户在涉及多进程编程时,一个折中的解决方案,提出和解决了很多多进程开发时存储数据遇到的问题:数据同步、进程间通信效率、数据是否需要上锁等。
- 数据同步:多进程下因为是固定的内存大小区域,所以每个进程读取和写入都是只有一份数据的,不存在数据不同步的问题。
- 进程间通信:因为多个进程共享一片区域的内存,所以不需要进程间通信,无协程切换。
- 镀锡是否需要上锁:看情况。一般情况下 Swoole/Table 模块自带一个行锁,只有两个进程在两个 CPU 上同时读取一行数据时才会发生抢锁,作为框架的使用者,如果只写或只读,是无需手动上任何锁的。只有在先 `get()``set()` 这样的情况才需要上自旋锁。后面的段会详细讲述。
使用体验上,基本和 ZMBuf 无差,如果没有用过 1.x 的版本,可无视此段话。
## 使用
### 配置和初始化
配置文件还是在 `config/global.php` 文件里,字段是 `light_cache`
```php
/** 轻量字符串缓存,默认开启 */
$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
];
```
其中 `$size` 是最多保存的键值对数目,填写非 2 的倍数时底层会自动修正为 2 的倍数值。
`$max_strlen` 为单条值最长保存的长度。因为 Swoole/Table 只能存数字、字符串所以在存取数组等变量时会先将其序列化为字符串形式保存get 时自动反序列化回来。在存数组等非字符串变量时,请先自行计算你要存取的内容序列化后的最大长度。如果长度超出最大长度,则无法保存,`set()` 将返回 false。
`hash_conflict_proportion`Table 模块底层使用 hash 表,会存在 hash 冲突,调大 Hash 冲突率会提升 `size` 指定条目数的准确性,但也将增加物理内存的使用。这里单位是百分比,`0.6``60%`
`persistence_path` 是持久化保存变量的文件保存位置,默认在 `zm_data/_cache.json` 文件。
`auto_save_interval` 是持久化保存变量的自动保存时间,单位秒。
### LightCache::set()
设置内容。
定义:`LightCache::set($key, $value, $expire = -1)`
返回值:`bool`。当 value 超出了最大长度或内存不足时,返回 false其余 true。
参数:
`$key` 的长度不能超过 64 字节,且不能存入二进制内容。
`$value` 可存入 `bool``string``int``array` 等可被 `json_encode()` 的变量,闭包函数和对象不可存入。
`$expire``int`,超时时间(秒)。如果设定了大于 0 的值,则表明是在 `$expire` 秒后自动删除(框架中途停止不受影响)。如果为 -1 则什么都不做。框架停止后自动被清除。
**注意:如果前面使用了 set() ,后面再次使用 set() 会重置 expire 过期时间为 -1-1 是框架运行时不过期,关闭框架删除的状态),如果只需要更新值,请使用 update()。**
```php
// use ZM\Store\LightCache;
/**
* @CQCommand("store")
*/
public function store() {
LightCache::set("key1", ["value1" => "strOrInt", "value2" => 123]);
return "OK!";
}
/**
* @CQCommand("storeAfterRemove")
*/
public function storeAfterRemove() {
LightCache::set("store1", "remove1", 30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
zm_sleep(30);
ctx()->reply(LightCache::get("store1") !== null ? "内容存在!" : "内容不存在!");
}
```
<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()
获取内容。
返回值:`mixed|null`。当无内容或过期时返回 null剩余情况返回原数据。
### LightCache::getExpire()
获取存储项剩余过期时间(秒)。
定义:`LightCache::getExpire(string $key)`
```php
$s = LightCache::set("test", "hello", 20);
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();
```
轻量缓存的内存手工计算方式:(Table 结构体长度` + `KEY 长度 64 字节 + `$size`) * (1 + `$conflict_proportion`) * 列尺寸。
Table 结构体长度根据你所设定的 `max_strlen` 会变化。
> 框架默认配置下的轻量缓存启动后大约占用内存 25MB 左右。
### LightCache::isset()
判断某项是否存在。
```php
LightCache::set("foo", "bar");
dump(LightCache::isset("foo")); // true
```
### LightCache::unset()
删除某项。
```php
LightCache::set("foo", "bar");
LightCache::unset("foo");
dump(LightCache::isset("foo")); // false
```
### LightCache::getAll()
获取所有项。
```php
LightCache::set("k1", ["I", "am", "array"]);
LightCache::set("k2", "v2");
LightCache::set("k3", 20001);
dump(LightCache::getAll());
/*
{
"k1": ["I", "am", "array"],
"k2": "v2",
"k3": 20001
}
*/
```
### LightCache::addPersistence()
添加持久化存储的键。
用法:`LightCache::addPersistence($key)`
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为 -2 了。2.4.2 起可用此方法)。
详见下方 **持久化**
### LightCache::removePersistence()
删除持久化的键。
用法:`LightCache::removePersistence($key)`
注:只需调用一次即可,无需多次重复调用,也不需要设置 expire 为非 -2 了。2.4.2 起可用此方法)。
### 持久化
使用 `LightCache::addPersistence($key)` 添加对应需要持久化的键名即可。
```php
/**
* @OnStart()
*/
public function onStart() {
LightCache::addPersistence("msg_time");
}
/**
* @CQCommand("getStore")
*/
public function getStore() {
return "存储时间:".date("Y-m-d H:i:s", LightCache::get("msg_time"));
}
```
<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>
### 数据加锁
在特定情况下,使用 LightCache 必须配合锁使用,否则会出现数据错乱。我们来看下面的例子:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
return "<h1>It works!</h1>";
}
```
我们使用压测工具,例如 `ab`,对此路由接口开很多很多线程进行测试,假设我们设置请求总数为 200000 次,框架的工作进程数为 8我用的是 2020 年末的 i5 MacBook Pro 13 inch
> 懒得再测了,下面就口述过程吧。
在运行完测试后,通过 `LightCache::get("web_count")`,获取到的数你会发现不是 200000。怎么回事呢请自行翻阅多进程开发相关的书籍哦或者简单理解为有一些情况下进程 1 执行到了 `if-else` 语句,另一个进程也执行到了这里,两次在代码层面加的数是相同的,则虽然请求了两次,但是后执行 set 的那个进程又覆盖了前一个进程执行的值,导致最终结果加了 1 而不是 2
::: tip 提示
同样的场景,使用 ZMAtomic 就不需要使用锁了。Atomic 是一句话:`add(1)` 立即加值的。而 LightCache 需要加锁的情况一般都是 `get->改值->set` 这样的代码。
:::
解决这一问题,就需要用到锁。这种情况下,我们首先考虑的是自旋锁,框架也因此内置了一个方便使用的自旋锁组件。详见下一章:自旋锁。
## 如何临时缓存大变量
由于 LightCache 需要提前声明最大大小,所以在某些情况下,比如第三方 API 接口结果临时缓存,可能不太适合使用,这时对于 2.x 版本的多进程炸毛框架是一个新的问题。
解决方案有三种:
-`global.php` 中的 `swoole.worker_num` 调整为 `1` 即可,所有除所有主 handler 事件的用户类外其他类均可使用如 `Hello::$store` 类似的静态变量全局存取
- 使用 WorkerCache需要 2.2.0 以上版本)
- 使用 Redis需要安装 `redis` 扩展)
以上WorkerCache 是为了弥补 LightCache 的不足而诞生的,以下就是 WorkerCache 的具体内容。
### WorkerCache 跨进程大缓存
WorkerCache 和 LightCache 几乎完全不同WorkerCache 存储的方式说白了就是 PHP 的静态变量,不过框架支持使用封装好的进程间通信进行跨进程读取。但由于需要设置一个存储变量的进程,所以配置文件必须先指定要将数据存到哪个 Worker/TaskWorker 进程中。关于框架内多进程的说明,请见 [进阶 - 多进程 Hack](/advanced/multi-process)。
定义:`ZM\Store\WorkerCache`
#### 配置
见 [基本配置](/guide/basic-config)。
#### WorkerCache::get()
定义:`get($key)`
`$key` 为指定要获取的键值对的值,如果不存在则返回 null。
#### WorkerCache::set()
定义:`set($key, $value, $async = false)`
设置变量,你懂的。
注意,`$value` 可以是被无损 `json_encode``json_decode` 的变量闭包Closure、资源resource等类型不支持存储。
`$async` 默认为 false当为 true 时候,不会返回是否成功设置与否,否则会协程等待是否目标进程存储成功。
#### WorkerCache::unset()
定义:`unset($key, $async = false)`
删除键对应的值。`$async` 的意义同上。
#### WorkerCache::add()
定义:`add($key, int $value, $async = false)`
给 int 类型的值加一,如果值不存在,则默认为 0 且加上目标的 `$value`
#### WorkerCache::sub()
定义:`sub($key, int $value, $async = false)`
给 int 类型的值减一,如果值不存在,则默认为 0 且减去目标的 `$value`
```php
<?php
namespace Module\Example;
use ZM\Store\WorkerCache;
use ZM\Annotation\CQ\CQCommand;
class Hello {
/**
* @CQCommand("set_store")
*/
public function setStorage() {
$arg1 = ctx()->getNextArg("请输入要设置的内容名称");
$arg2 = ctx()->getFullArg("请输入要设置的内容");
WorkerCache::set($arg1, $arg2);
return "成功!";
}
/**
* @CQCommand("get_store")
*/
public function getStorage() {
$arg1 = ctx()->getFullArg("请输入要获取的内容名称");
$data = WorkerCache::get($arg1);
return $data ?? "内容不存在!";
}
}
```
<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>

View File

@@ -0,0 +1,101 @@
# MySQL 数据库(旧版组件)
::: warning 注意
此 MySQL 组件为旧版 MySQL 查询器组件,为了统一和提升对未来独立组件的兼容性,现转变为使用 `doctrine/dbal``doctrine/orm` 库来实现查询器,请转到 [MySQL 查询器]()。
:::
## 配置
炸毛框架的数据库组件支持原生 SQL、查询构造器去掉了复杂的对象模型关联同时默认为数据库连接池使开发变得简单。
数据库的配置位于 `config/global.php` 文件的 `sql_config` 段。
数据库操作的唯一核心工具类和功能类为 `\ZM\DB\DB`,使用前需要配置 host 和 use 此类。
## 查询构造器
在 炸毛框架 中,数据库查询构造器为创建和执行数据库查询提供了一个方便的接口,它可用于执行应用程序中大部分数据库操作。同时,查询构造器使用 `prepare` 预处理来保护程序免受 SQL 注入攻击,因此没有必要转义任何传入的字符串。
### 新增数据
```php
DB::table("admin")->insert(['admin_name', 'admin_password'])->save();
// INSERT INTO admin VALUES ('admin_name', 'admin_password')
```
其中 `insert` 的参数是插入条目的数据列表。假设 admin 表有 `name``password` 两列。
> 自增 ID 插入 0 即可。
### 删除数据
```php
DB::table("admin")->delete()->where("name", "admin_name")->save();
// DELETE FROM admin WHERE name = 'admin_name'
```
其中 `where` 语句的第一个参数为列名,当只有两个参数时,第二个参数为值,效果等同于 SQL 语句:`WHERE name = 'admin_name'`,当含有第三个参数且第二个参数为 `=``!=``LIKE` 的时候,效果就是 `WHERE 第一个参数 第二个参数的操作符 第三个参数`
### 更新数据
```php
DB::table("admin")->update(["name" => "fake_admin"])->where("name", "admin_name")->save();
// UPDATE admin SET name = 'fake_admin' WHERE name = 'admin_name'
```
`update()` 方法中是要 SET 的内容的键值对,例如上面把 `name` 列的值改为 `fake_admin`
### 查询数据
```php
$r = DB::table("admin")->select(["name"])->where("name", "fake_admin")->fetchFirst();
// SELECT name FROM admin WHERE name = 'fake_admin'
echo $r["name"];
echo DB::table("admin")->where("name", "fake_admin")->value("name");
// SELECT * FROM admin WHERE name = 'fake_admin'
```
`select()` 方法的参数是要查询的列,默认留空为 `["*"]`,也就是所有列都获取,也可以在 table 后直接 where 查询。
其中 `fetchFirst()` 获取第一行数据。
如果只需获取一行中的一个字段值,也可以通过上面所示的 `value()` 方法直接获取。
多列数据获取需要使用 `fetchAll()`
```php
$r = DB::table("admin")->select()->fetchAll();
// SELECT * FROM admin WHERE 1
foreach($r as $k => $v) {
echo $v["name"].PHP_EOL;
}
```
### 查询条数
```php
DB::table("admin")->where("name", "fake_admin")->count();
//SELECT count(*) FROM admin WHERE name = 'fake_admin'
```
## 直接执行 SQL
> 在查询器外执行的 SQL 语句都不会被缓存,都是一定会请求数据库的。
### DB::rawQuery()
- 用途:直接执行模板查询的裸 SQL 语句。
- 参数:`$line``$params`
- 返回:查到的行的数组
`$line` 为请求的 SQL 语句,`$params` 为模板参数。
```php
$r = DB::rawQuery("SELECT * FROM admin WHERE name = ?", ["fake_admin"]);
//SELECT * FROM admin WHERE name = 'fake_admin'
echo $r[0]["password"];
```
> 参数查询已经从根本上杜绝了 SQL 注入的问题。

View File

@@ -0,0 +1,3 @@
# MySQLStatement
你好啊,这里是 Statement。TODO

View 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)
```

View File

@@ -0,0 +1,62 @@
# Redis
炸毛框架内置了 Redis 连接池,供开发者使用。使用前需要先安装 `redis` 扩展:
```bash
pecl install redis
```
> 如果是 Docker 环境,则默认已安装。
## 配置
配置文件在 `config/global.php` 的全局配置文件下,详情见 [配置](/guide/basic-config/#redis_config)。
示例配置(假设 Redis Server 开到了本地):
```php
/** Redis连接信息host留空则启动时不创建Redis连接池 */
$config['redis_config'] = [
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 1,
'db_index' => 0,
'auth' => ''
];
```
## 使用
当写好配置文件后,不可以使用 reload 进行重载,因为连接池需要在主进程中声明配置,才可以应用到多个工作进程中。所以必须输入 `stop` 或 Ctrl+C 停止后再启动框架。
定义:`ZM\Store\Redis\ZMRedis`
因为使用的是连接池,所以每次使用完一个连接需要归还连接给连接池。框架封装了两种方式自动归还,你可以选择下面其中的任意一种。
以下的方式获取的 `$redis` 都是 `redis` 扩展的对象 `\Redis`,关于 redis 扩展的方法文档,详情见:[Redis 文档](https://www.php.cn/course/49.html)。
### 对象模式
```php
$obj = new ZMRedis();
$redis = $obj->get();
ctx()->reply($redis->ping("123"));
```
### 回调模式
```php
// 前面的代码
ZMRedis::call(function($redis) {
$redis->set("key1", "hello world");
$result = $redis->get("key1");
ctx()->reply($result);
});
// 后面的代码
```
### 二者的区别
选一个喜欢的就好。硬要是说区别的话,对象模式是在 PHP 自动回收这个 `ZMRedis` 对象时会归还连接,也可以通过手动 `unset($obj)` 进行回收,否则就会执行到函数结尾自动回收。切记不可将 `$obj` 对象持久化存到静态或全局变量等。
回调模式看似是回调,但是是同步执行的,不会发生顺序错乱。也就是说到了 `ZMRedis::call()` 方法里面的时候,后面的代码不会提前执行,是顺序执行的。回调的作用仅仅是用作自动回收连接对象。

View File

@@ -0,0 +1,81 @@
# SpinLock 自旋锁
前面讲到 LightCache 轻量缓存在特定的情况下为了保证数据不被多进程的因素导致丢失或覆盖,在高并发情况下修改数据需要加锁,所以炸毛框架内置了 SpinLock 自旋锁。
::: tip 提示
框架单进程运行的模式下不需要任何自旋锁。
:::
## 配置
自旋锁使用无需配置,和 LightCache 同源。
## 使用
定义:`ZM\Store\Lock\SpinLock`
### SpinLock::lock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则原地等待直到其他资源释放锁。
```php
SpinLock::lock("foo");
```
### SpinLock::unlock($key)
给信号量 `$key` 释放锁。
```php
SpinLock::unlock("foo");
```
### SpinLock::tryLock($key)
给信号量 `$key` 上锁。如果该信号量已经被上锁,则立刻返回 false。
```php
SpinLock::trylock("foo");
```
## 综合实例
我们这里以之前在 LightCache 中的实例进行继续讲解,如何给之前那样的情况加锁:
```php
/**
* @RequestMapping("/test")
*/
public function test() {
SpinLock::lock("web_count"); // 加上这行
$s = LightCache::get("web_count");
if($s === null) $s = 1;
else $s += 1;
LightCache::set("web_count", $s);
SpinLock::unlock("web_count"); // 再加上这行
return "<h1>It works!</h1>";
}
```
加两行就 OK。这时再使用压测工具请求 200000 次,值就会是 200000 了!
原理剖析:在 LightCache 获取前,先对此内容上锁,这时如果其他进程有同时也在执行这个代码的时候,就会在 `SpinLock::lock()` 这行代码处原地等待,防止继续执行。等前面的那个进程执行到 `SpinLock::unlock()` 释放锁时,其他进程才可继续执行,从而避免了多个进程并行执行这段代码导致的数据错乱。
::: danger 警告
使用锁时务必谨慎,如果不按照下面的规则使用自旋锁可能导致 CPU 占用率上升。
:::
自旋锁使用约定:
- 使用 `SpinLock::lock()` 指定信号量名称时必须指定为字符串,且最好与你的 LightCache 缓存名称相同。
- 使用 `lock()` 时最好紧跟在 `LightCache::get()` 代码前。
- 使用自旋锁后,`LightCache::get()``LightCache::set()` 之间的代码段一定不能有 **读写文件、数据库操作和网络请求** 等代码,最好为纯 PHP 逻辑代码,且越短越好,如示例代码。
-`LightCache::set()` 后最好紧跟 `SpinLock::unlock()`
## 性能
使用自旋锁几乎没有性能损失,自旋锁要比其他类型的锁性能强很多,在上方举例使用的 `ab` 压测工具测试 100万请求 下使用自旋锁和不适用自旋锁的测试成绩时间分别为7.4s 和 6.9s。

View File

@@ -0,0 +1,3 @@
# 自定义注解
TODO师傅莫催快肝完了

View File

@@ -0,0 +1,119 @@
# 事件分发器(进阶)
事件分发器是以上所有注解事件执行函数的一个分发器,如果你在上一章已经学会了如何创建自定义注解,那么本章就来说明如何用内置的事件分发器进行分发自定义事件。
如果你不需要了解或自定义有关事件分发的功能,此处可无需阅读。
## 属性
- 类名:`ZM\Event\EventDispatcher`
## 方法
### EventDispatcher::interrupt()
阻断当前正在运行的事件,只能在事件内部被调用的函数中实现。
### __construct()
构造方法。
```php
EventDispatcher::__construct(string $class = '')
```
初始化一个事件分发器,可进行一系列设置,对事件分发做限定。
#### 参数
`$class`:设置要分发的事件对应的注解类名,支持自定义注解(例如 `CQMessage::class`
### setRuleFunction()
设置函数触发规则判定的函数(就是在执行事件函数前执行的规则判定)
```php
setRuleFunction(callable $rule = null)
```
#### 参数
`$rule`:支持回调或闭包。闭包的参数为执行对应事件函数所绑定的注解事件对象。
```php
$dispatcher = new EventDispatcher(CustomEvent::class);
$dispatcher->setRuleFunction(function($obj) {
return $obj->name == "zhamao" ? true : false;
});
```
上方的 `$obj` 就是 CustomEvent 类的实例,参数绑定为注解中对应的参数。
### setResultFunction()
设置事件函数返回值处理的回调函数。
```php
setReturnFunction(callable $return_func)
```
#### 参数
`$return_func`:设置事件函数返回值处理的回调函数,回调参数绑定为对应单独事件函数的返回值。
```php
$dispatcher = new EventDispatcher(CustomEvent::class);
$dispatcher->setReturnFunction(function($return) {
if (is_string($return)) Console::info("函数返回了 ".$return);
});
```
### dispatchEvents()
开始分发事件。
```php
dispatchEvents(...$params)
```
#### 参数
自定义参数,这里填入的参数将被填入被分发的函数参数中。
```php
$dispatcher->dispatchEvents("foo", "bar");
```
```php
<?php
class Test {
/**
* @CustomEvent("zhamao")
*/
public function test($arg1, $arg2) {
echo "$arg1: $arg2"; //将输出 "foo: bar"
}
}
```
## 机制
事件分发器的机制说简单不简单,说复杂也不复杂,它和中间件有着非常大的关系,因为它会自动检测和识别所要执行的函数有没有中间件,并且根据顺序进行执行。
在炸毛框架内部,一个完整的事件流程和中间件的关系如下图:
![Untitled Diagram](https://static.zhamao.me/images/docs/dbb4e32e1c77f96162d10e41f25befa4.png)
对于同一事件的优先级和响应顺序,优先级的关系如下图:
![diagram](https://static.zhamao.me/images/docs/fa52005b7ca891053617a77541c7e785.png)
对于事件内单个事件被调用的单个函数下如果存在多个中间件,中间件模型和事件的关系如下图:
![Untitled Diagram-2](https://static.zhamao.me/images/docs/16ce39caad472d03d7786e6ffb0c55bf.png)
## 实战例子
我们假设 CustomEvent 是我们的自定义注解。还没写完,这部分太复杂了,而且举例子也不好举例,这块应该也不用着急更新。
TODO待完成

View File

@@ -0,0 +1,433 @@
# 框架核心注解事件
框架核心注解事件区别于机器人和路由注解事件,这里框架注解事件都是**直接**或封装调用 Swoole 的回调事件的,所以对一些比较底层或者基础的操作都在这里做,例如收到 HTTP 或 WebSocket 连接后执行的事件函数。
## OnOpenEvent()
当有 WebSocket 连接接入框架时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------- |
| 名称 | `@OnOpenEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnOpenEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnOpenEvent("foo")
@OnOpenEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnCloseEvent()
当有 WebSocket 连接断开框架时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------- |
| 名称 | `@OnCloseEvent` |
| 触发前提 | 当有 WebSocket 连接断开框架时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnCloseEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnCloseEvent("foo")
@OnCloseEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnRequestEvent()
当 HTTP 请求接入时,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------- |
| 名称 | `@OnRequestEvent` |
| 触发前提 | 当 HTTP 请求接入时,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnRequestEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | --------------------------------------------- | ------------------------ | ---------------- |
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 前置条件 | 空rule 为 true |
| level | `int` | 事件优先级(越大越靠前) | 20 |
## OnMessageEvent()
当有 WebSocket 连接接入框架后发送过来消息,触发注解事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------- |
| 名称 | `@OnMessageEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架后发送过来消息,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnMessageEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | -------- | ------------------------------------------------------------ | ---- |
| connect_type | `string` | 限定连接的类型,通过炸毛框架支持的方式指定传入类型,详见 [进阶 - 接入 WebSocket 客户端](/advanced/connect-ws-client) | |
### 用法
```java
@OnMessageEvent("foo")
@OnMessageEvent(connect_type="default")
```
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnPipeMessageEvent()
当有 其他 Worker 进程通信发来指令激活响应。2.2.0 版本可用)
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------- |
| 名称 | `@OnPipeMessageEvent` |
| 触发前提 | 当有 WebSocket 连接接入框架后发送过来消息,触发注解事件 |
| 命名空间 | `ZM\Annotation\Swoole\OnPipeMessageEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------- | ------------ | ---- |
| action | `string` | 限定动作名称 | |
### 用法
```java
@OnPipeMessageEvent("foo")
@OnPipeMessageEvent(action="bar")
```
### 事件绑定参数
`$data`: 数组,内容如下:
```php
[
"action" => "你的上面的名称",
... //其他自己发送时随便定义,带什么都行
]
```
## OnSwooleEvent()
绑定 Swoole 所相关的事件,例如 WebSocket 接入、收到 WS 消息、关闭 WS 连接HTTP 请求到达等。这个是旧的统一的 Swoole 事件分发注解。**请尽量使用上面几个新的注解**。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------ |
| 名称 | `@OnSwooleEvent` |
| 触发前提 | 当参数指定的 `type` 对应的事件被触发后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSwooleEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------------------------------------------------------- | ----------------------------------------------- | ---------------- |
| type | `string`,支持填入 `open``request``close``message` | 限定事件的类型,**必填** | |
| rule | `string`,必须是可执行且返回 bool 的 PHP 代码 | 例如判断连接是否为 QQ 机器人(`connectIsQQ()` | 空rule 为 true |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 事件绑定参数
`$conn`: [ConnectionObject](/advanced/connect-ws-client/) 类型,返回一个当前 WS 连接的连接对象。
## OnStart()
在框架加载后执行的注解事件,用于初始化 Worker 进程,此注解事件会在 Worker 进程中执行,且可以指定在哪个 Worker 进程中执行。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------ |
| 名称 | `@OnStart` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnStart` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------------------ | ---- |
| worker_id | `int`,要在哪个 Worker 进程上执行,默认为 0范围是 0{你设定的 Worker 数量-1},如果是 -1 的话,则会在所有 Worker 进程上触发。 | 限定只执行的 Worker 进程 | |
## OnTick()
在框架加载后创建毫秒计时器。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------- |
| 名称 | `@OnTick` |
| 触发前提 | 在框架加载后激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnTick` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------- | ------------------------------------------------------------ | ------------------------ | ---- |
| 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()
在框架加载前执行的代码。此部分代码是在主进程执行的,不可在此事件中使用任何协程相关的功能。
比如我们要改变所有进程的 ini 设置,这时使用 `@OnStart(-1)` 这样只设置了 Worker 进程的内容,而主进程和管理进程无法被覆盖到。如果需要设置全局的一些配置,务必在此 `@OnSetup` 注解下执行。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------ |
| 名称 | `@OnSetup` |
| 触发前提 | 在框架加载前激活 |
| 命名空间 | `ZM\Annotation\Swoole\OnSetup` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
无。
## TerminalCommand()
添加一个远程终端的自定义命令。2.4.0 版本起可用)
### 属性
| 类型 | 值 |
| ---------- | --------------------------------------- |
| 名称 | `@TerminalCommand` |
| 触发前提 | 连接到远程终端可触发 |
| 命名空间 | `ZM\Annotation\Command\TerminalCommand` |
| 适用位置 | 方法 |
| 返回值处理 | 无 |
### 注解参数
| 参数名称 | 参数范围 | 默认 |
| ----------- | ------------------------------ | ---- |
| command | `string`**必填**,命令字符串 | |
| alias | `string`,可选,命令的别名 | |
| description | `string`,要显示的帮助文本 | 空 |
## 示例1机器人连接框架后输出信息
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnOpenEvent;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
class Hello {
/**
* 在机器人客户端连接框架后向终端输出信息
* @OnOpenEvent("qq")
* @param $conn
*/
public function onConnect(ConnectionObject $conn) {
Console::info("机器人 " . $conn->getOption("connect_id") . " 已连接!");
}
}
```
这里的 Console 是终端输出组件,详情见组件一栏对应的文档查询。
## 示例2阻断 Chrome 访问框架时多访问一次的问题)
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnSwooleEvent;
use ZM\Event\EventDispatcher;
class Hello {
/**
* 阻止 Chrome 自动请求 /favicon.ico 导致的多条请求并发和干扰
* @OnRequestEvent(rule="ctx()->getRequest()->server['request_uri'] == '/favicon.ico'",level=200)
*/
public function onRequest() {
EventDispatcher::interrupt();
}
}
```
其中 EventDispatcher 为事件分发器interrupt 是通用阻断方法,如果你平常只使用阻断,则只需掌握这一个方法即可,`EventDispatcher::interrupt()` 在所有事件内可用。
## 示例3接收 WS 客户端发来的数据)
见 [接入 WebSocket 客户端](/advanced/connect-ws-client)。
## 示例4使用 OnStart 给所有 Worker 进程写入缓存提速)
如果你有一些数据存到了文件、数据库中,且是只读不写的,那么就可以使用此方法将这个文件或者数据库的内容读入 Worker 进程的内存中进行使用来提速。
假设我们有一个大文件 json里面存着一份题库例如
```json
{
"0": {
"question": "法的调整对象是( )。",
"answer": {
"A": "行为关系",
"B": "思想关系",
"C": "利益关系",
"D": "各种社会资源"
},
"key": "A",
"answer_type": 0
},
"1": {
"question": "法律与其他社会规范的区别在于( )。",
"answer": {
"A": "是调整人们行为的规范",
"B": "有约束力",
"C": "由国家强制力保证执行",
"D": "规定制裁措施"
},
"key": "C",
"answer_type": 0
}
}
```
那么我们可以使用 OnStart 来实现一个,将此文件读取到每个 Worker 进程中,并且快速取用的功能(以下做了一个简单的查题功能):
```php
<?php
namespace Module\Example;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\CQ\CQCommand;
use ZM\Console\Console;
class Hello {
public static $tiku = [];
/**
* @OnStart(-1)
*/
public function onStart() { // 注意,此函数将会在每个 Worker 执行一次
$file = file_get_contents("tiku.json"); //从文件读取json
$json = json_decode($file, true); //json解析
Hello::$tiku = $json; //将解析后的数组以静态变量的方式存到每个 Worker 的内存中
Console::success("加载题库完成!");
}
/**
* @CQCommand("找题")
*/
public function findQuestion() {
$tiku_id = ctx()->getNumArg("请输入题目的序号");
if(!isset(Hello::$tiku[$tiku_id])) return "题目id为".$tiku_id."的题目不存在!";
$timu = Hello::$tiku[$tiku_id];
$msg = "题目名称:".$timu["question"];
foreach($timu["answer"] as $k => $v) {
$msg .= "\n".$k.". ".$v;
}
$msg .= "\n正确答案:".$timu["key"];
return $msg;
}
}
```
终端效果:(我们假设运行框架的电脑是四核 CPU
```log
[14:28:00] [S] [#0] 加载题库完成!
[14:28:00] [S] [#2] 加载题库完成!
[14:28:00] [S] [#1] 加载题库完成!
[14:28:00] [S] [#3] 加载题库完成!
```
聊天效果:
<chat-box :my-chats="[
{type:0,content:'找题 1'},
{type:1,content:'题目名称:法律与其他社会规范的区别在于( )。\nA. 是调整人们行为的规范\nB. 有约束力\nC. 由国家强制力保证执行\nD. 规定制裁措施\n正确答案C'},
]"></chat-box>
## 示例5创建每分钟自动执行的爬虫
```php
/**
* @OnTick(tick_ms=60000,worker_id=0)
*/
public function onCrawl() {
$data = Foo::bar(); //这里是你自己写的要爬的接口等等一系列操作
LightCache::set("your_data_key_name", $data); //将爬虫数据存入 LightCache 轻量缓存
}
```
## 示例6创建一个远程终端命令并调试框架
> 开个坑以后填。__填坑标记__
>

View File

@@ -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 代码中给函数贴上一个可以影响它一生并且改变它行为的标签,而有了注解,就相当于有了给函数贴标签的机会。
在常见框架如 SpringSwoft 等代码结构里面,注解更是其核心的存在。
在炸毛框架中,我们所有事件的绑定均采用这一方式进行调用模块内各个方法。包括 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,10 +53,37 @@ 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`
等这类注解显然不代表任何事件,只能当作这个函数或类的修饰属性而已。代表了事件的注解,我们称之为**注解事件**,它会在某种事件达成条件后触发注解下方的函数本身。
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
值得注意的是,注解事件本身概念是我凭空捏造的,我不好解释所以只能创造这么一个词来代指这一抽象的概念,硬要解释的话,大致就好比一个社区里有一个卖牛奶的,有几家人订阅了每日上门送牛奶的服务,只要你打了“给我配送牛奶”的注解,他就会上门。而它送的不止一种奶,可以给你个性化定制,比如让卖牛奶的给你带包糖带瓶水,而描述这个的注解就只能做一个之前注解的修饰。假设你只写了带包糖的注解,没有写给我配送牛奶的注解,那他永远也不会给你送牛奶和糖过来。
## 阻断事件
由于炸毛框架内的注解事件统一由一个通用的事件分发器进行分发,所以你在任何注解事件内都可以用通用的方式阻断当前正在运行的事件。
首先就是要记得先 use 事件分发器的类:`use ZM\Event\EventDispatcher;`
```php
EventDispatcher::interrupt();
EventDispatcher::interrupt($data); // 也可以带返回值,自定义注解事件时有用。
```

194
docs/event/middleware.md Normal file
View File

@@ -0,0 +1,194 @@
# 中间件注解
对于 `@RequestMapping` 等注解绑定的事件函数,还支持中间件,可以完成 Session 会话、认证、日志记录等功能。中间件是用于控制 `请求到达``响应请求` 的整个流程的。从一定意义上来说相当于切面编程AOP
在炸毛框架中,中间件最直白的意思就是注解事件执行前、执行后、执行过程中可进行插入代码但不破坏原有代码。
```
@中间件1
@带条件的注解1
function 我的方法() {
blablabla...
}
//插入中间件,下面是执行流程
-> 判断注解1的执行条件是否为true
-> 中间件1的前置插入代码
-> 我的方法
-> 中间件1的后置插入代码
X -> 我的方法有异常时执行中间件1的异常处理
//不插入中间件,下面是执行流程
-> 判断注解1的执行条件是否为true
-> 我的方法
X -> 有异常则直接跳到最外层被框架捕获
```
中间件和事件分发器是紧密相连的,炸毛框架的内部分发器在分发注解事件的过程中会判断将要执行的事件是否含有中间件,框架内部执行流程图见下一章:事件分发器。
## 定义中间件
下方就是一个可以在终端打印路由函数运行的总时间的中间件,只需给中间件标明里面的 `@MiddlewareClass` 到中间件的类上就可以了。
```php
<?php
namespace Module\Middleware;
use Exception;
use ZM\Annotation\Http\HandleAfter;
use ZM\Annotation\Http\HandleBefore;
use ZM\Annotation\Http\HandleException;
use ZM\Annotation\Http\MiddlewareClass;
use ZM\Console\Console;
use ZM\Http\MiddlewareInterface;
/**
* @MiddlewareClass("timer")
*/
class TimerMiddleware implements MiddlewareInterface
{
private $starttime;
/**
* @HandleBefore()
* @return bool
*/
public function onBefore() {
$this->starttime = microtime(true);
return true;
}
/**
* @HandleAfter()
*/
public function onAfter() {
Console::info("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms.");
}
/**
* @HandleException(\Exception::class)
* @param Exception $e
* @throws Exception
*/
public function onException(Exception $e) {
Console::error("Using " . round((microtime(true) - $this->starttime) * 1000, 2) . " ms but an Exception occurred.");
throw $e;
}
}
```
技术要素:
1. 将需要声明为中间件的 class 类标上注解 `@MiddlewareClass`,并带有参数,参数为中间件名称,字符串即可。
2. 使用 `@MiddlewareClass` 的需要先 use`use ZM\Annotation\Http\MiddlewareClass;`
3. 类成员中声明执行前插入、执行后插入和异常捕获函数也需要注解,分别是 `@HandleBefore``@HandleAfter``@HandleException`,都在 `ZM\Annotation\Http` 命名空间下。
4. `@HandleBefore` 类似 `@CQBefore`,需要返回 bool 类型值,如果不返回,默认为 true。当为 true 时,则不会阻断执行事件函数本身。
5. 中间件内的函数不可被绑定为注解事件。
6. `@HandleException` 可以写多个,但其中的参数只能写想要捕获的异常的类全称,例如 `\Exception::class` 返回的就是 `\\Exception``\ZM\Exception\InterruptException::class` 返回的是 `ZM\\Exception\\InterruptException`,举的这两个例子这样写都是可以的。
7. 如果 `@HandleException` 有多个的话,则会按照声明顺序依次让其捕获,看其是否为要被捕获的错误的类或父类。例如在最后一个 `@HandleException` 捕获 `\Throwable` 则最终此中间件会捕获所有异常。
8. 中间件内可以正常使用和注解事件执行的内容同一上下文,例如 `@RequestMapping` 下你可以使用 `ctx()->getRequest()``@CQMessage` 可以使用 `ctx()->getMessage()` 等,以此类推。
## 使用中间件
如上图,我们举了一个非常简单的例子,打印出函数执行的时间。我们假设一个需要耗时较长的函数:
```php
/**
* @RequestMapping("/testTime")
* @Middleware("timer")
*/
public function testTime() {
zm_sleep(3); //等待3秒再返回
return "OK!";
}
```
在执行后,你的执行结果可能为:
```
[11:18:56] [I] [#0] Using 3000.07 ms
```
或者,我们也可以将中间件注解写到类上:
```php
/**
* @Middleware("timer")
*/
class Hello {
/**
* @RequestMapping("/test/ping")
*/
public function ping(){
return "pong";
}
/**
* @RequestMapping("/test/ping2")
*/
public function ping2(){
return "pong2";
}
}
```
效果等同于给此类下每个注解事件写一个 `@Middleware`
## 使用多个中间件
多个使用中间件可以同时生效多个流程的中间件。这里要注意,多个中间件中,`@HandleBefore` 方法中如果返回了 `false`,则不会执行接下来的中间件和事件本身要触发的函数,直接跳到最后此中间件的 `@HandleAfter` 方法。
```php
/**
* @CQCommand("你好")
* @Middleware("timer1")
* @Middleware("timer2")
*/
public function hello() { return "成功执行!"; }
```
## 使用中间件捕获异常
通常情况下,如果用户定义的函数内抛出了异常(包括 `message` 等事件),会返回到框架基层去返回默认定义的内容。如果想自己捕获可以使用 `try/catch` ,但不方便复用,多处使用的话就需要重复写代码。这里可以使用中间件的异常处理方便地捕获错误。这个函数写到中间件类里即可
```php
/**
* @HandleException(\Exception::class)
* @param Exception|null $e
*/
public function onThrowing(?Exception $e) {
ctx()->getResponse()->endWithStatus(500, "Error on this.");
}
```
这里的 `@HandleException` 中的参数为要捕获的类名,注意这里面的类名的命名空间需要写全称,不能上面 use 再使用,否则会无法找到异常类。
`ctx()` 为获取当前协程空间绑定的 `request``response` 对象。
## 中间件加载错误处理策略
中间件在某些情况下可能会产生普通 PHP 异常以外的异常,不能被框架的正常错误流程捕获,所以这里额外说明了中间件异常处理的几种策略。
中间件异常处理策略可以在 2.5.2 版本之后通过 `global.php` 中的 `runtime``middleware_error_policy` 设置。
- **0**: 无论被运行事件中间件是否存在,都不抛出异常,继续执行事件。
- **1**: 在框架启动时如果某事件被注解了一个不存在的中间件,则不抛出异常,在执行期间才检测是否存在此中间件,并抛出异常。
- **2**: 严格的中间件检查,在框架启动时就检测所有被注解了中间件的注解事件。
假设我们有一个路由注解 `@RequestMapping("/test")`,同时注解了一个不存在的中间件,如下:
```php
/**
* @RequestMapping("/test")
* @Middleware("foo")
*/
public function testRoute() {
return "I am testing middleware";
}
```
配置项为 0此中间件类不存在的话则会报告 warning并直接执行此函数。
配置项为 1在访问此路由执行此函数时会抛出异常中断此次事件。
配置项为 2在框架启动时抛出致命异常。

View File

@@ -0,0 +1,354 @@
# 机器人注解事件
QQ 机器人事件是指 CQHTTP 插件发来的 Event 事件,被框架处理后触发到单个类中方法的事件。
为了便于开发,这里的注解类对应 CQHTTP 插件返回的 `post_type` 类型,对号入座即可。
::: tip 提示
在使用注解绑定事件过程中,如果无 **必需** 参数,可一个参数也不写,效果就是此事件任何情况下都会调用此方法。例如:`@CQMessage()`
:::
事件是用户需要从 OneBot 被动接收的数据,有以下几个大类:
- [消息事件](#cqmessage),包括私聊消息、群消息等,被 [`@CQCommand`](#cqcommand)`@CQMessage` 注解处理。
- [通知事件](#cqnotice),包括群成员变动、好友变动等,被 `@CQNotice` 注解事件处理。
- [请求事件](#cqrequest),包括加群请求、加好友请求等,被 `@CQRequest` 注解事件处理。
- [元事件](#cqmetaevent),包括 OneBot 生命周期、心跳等,被 `@CQMetaEvent` 注解事件处理。
## 注解事件参照表
| 注解名称 | 类所在命名全称 | 作用 |
| ------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------ |
| [`@CQBefore`](/event/robot-annotations/#cqbefore) | `\ZM\Annotation\CQBefore` | OneBot 各类事件前触发的,可当作事件过滤器使用 |
| [`@CQAfter`](/event/robot-annotations/#cqafter) | `\ZM\Annotation\CQAfter` | OneBot 各类事件后触发的 |
| [`@CQMessage`](/event/robot-annotations/#cqmessage) | `\ZM\Annotation\CQMessage` | OneBot 中消息类事件的触发(机器人消息)事件 |
| [`@CQCommand`](/event/robot-annotations/#cqcommand) | `\ZM\Annotation\CQCommand` | OneBot 中消息类事件的触发(机器人消息)事件,但是被封装为指令型的,无需自己切割命令式 |
| [`@CQNotice`](/event/robot-annotations/#cqnotice) | `\ZM\Annotation\CQNotice` | OneBot 中通知类事件的触发(机器人消息)事件 |
| [`@CQRequest`](/event/robot-annotations/#cqrequest) | `\ZM\Annotation\CQRequest` | OneBot 中请求类事件的触发(机器人消息)事件,一般带有请求信息,可联动相关响应的 API 完成功能编写 |
| [`@CQMetaEvent`](/event/robot-annotations/#cqmetaevent) | `\ZM\Annotation\CQMetaEvent` | OneBot 中涉及 OneBot 实现本身的一些和机器人事件无关的元事件,比如 WS 连接的心跳包 |
## CQMessage()
QQ 收到消息后触发的事件对应注解。
### 属性
| 类型 | 值 |
| ---- | ----------- |
| 名称 | `@CQMessage` |
| 触发前提 | 当 `post_type``message` 时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQMessage` |
| 适用位置 | 方法 |
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------- | -------------------------------------- | ---- |
| message_type | `string`,支持填入 `private``group` | 限定消息事件的来源类型,如私聊或群消息 | 空 |
| user_id | `int64``string` | 限定消息发送用户 IDQQ 号) | 空 |
| group_id | `int64``string` | 限定消息发送来源群 IDQQ 群号) | 空 |
| message | `string` | 限定消息内容文本 | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
下面这个例子的注释用途就是:
- 在用户 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();
}
}
```
效果
<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()
此注解是对 `@CQMessage` 类别的再封装,是命令解析格式处理消息的利器。例如,你想写一个疫情上报,指令是 `疫情 城市名称`,那么此方式来解析用户消息会更加方便。
### 属性
| 类型 | 值 |
| ---------- | -------------------------------------------------------- |
| 名称 | `@CQCommand` |
| 触发前提 | 当根据参数规则匹配到用户命令式消息时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQCommand` |
| 适用位置 | 方法 |
| 返回值处理 | 当方法返回字符串时,效果等同于执行 `ctx()->reply("xxx")` |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------- | ------------------------------------------------------ | ---- |
| match | `string` | 匹配第一个词的命令式消息,如 `天气 北京` 中的 `天气` | 空 |
| pattern | `string` | 根据 * 号通配符进行模式匹配用户消息,如 `查询*天气` | 空 |
| regex | `string`,限定正则表达式 | 匹配正则表达式匹配到的用户消息 | 空 |
| start_with | `string` | 匹配消息开头相匹配的消息,如 `我叫炸毛`,这里写 `我叫` | 空 |
| end_with | `string` | 匹配消息结尾相匹配的消息,以 `start_with` 类推 | 空 |
| keyword | `string` | 匹配消息中有相关关键词的消息 | 空 |
| alias | `array[string]` | `match` 匹配到命令的别名,数组形式 | `{}` |
| message_type | `string`,支持填入 `private``group` | 限定消息事件的来源类型,同 `@CQMessage` | 空 |
| user_id | `int64``string` | 限定消息发送用户 ID`@CQMessage` | 空 |
| group_id | `int64``string` | 限定消息发送来源群 ID`@CQMessage` | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
::: warning 注意
`@CQCommand` 注解事件中,从 `match``keyword` 六个参数中,必须且只能定义一个,`alias` 目前只能和 `match` 参数同时使用;
框架内部对于同一条消息事件,优先处理 `@CQCommand` 注解事件,如果未匹配到任何注解事件,则才会继续执行 `@CQMessage` 注解事件。
:::
- 参数 `match` 匹配模式是:遇到空格、换行就会切分,比如 `点歌 xxx yyy` 会被分割为 `[点歌,xxx,yyy]`,然后抽取第一个词做为命令去匹配,剩下的为参数。
- 参数 `pattern` 匹配模式是:\* 号位置变成参数,比如 `从*到*的随机数`,我们输入 `从1到9的随机数`,成功匹配,参数列表:`[1,9]`
- 参数 `regex` 匹配模式为 PHP 标准的 pcre 正则表达式,比如 `([01][0-9][2][0-3]):[0-5][0-9]` 用来匹配 `22:45`
- 参数 `start_with` `end_with``keyword` 都是根据消息内容开头、结尾或者内容包含是否匹配来匹配,这里就不多说了,你懂的。
- 参数 `alias` 用的时候一般是这样:`@CQCommand(match="你好",alias={"你好啊","你是谁"})`,用以扩充同义词下命令的适配广度。
### 用法
我们以参数 `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);
}
}
```
效果
<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()
通知事件。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------- |
| 名称 | `@CQNotice` |
| 触发前提 | 当 `post_type``notice` 时触发(通知类事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQNotice` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ----------- | ------------------------------------ | ------------------------------------------------------------ | ---- |
| notice_type | `string`,支持填入 onebot 标准的内容 | 限定通知事件的类型,见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| user_id | `int64``string` | 限定通知事件用户 IDQQ 号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| group_id | `int64``string` | 限定通知事件群 IDQQ 群号),同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| operator_id | `int64``string` | 限定操作者 QQ 号,同上见 [OneBot - 通知事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/notice.md) | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQRequest()
请求事件。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------ |
| 名称 | `@CQRequest` |
| 触发前提 | 当 `post_type``request` 时触发(通知类事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQRequest` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| ------------ | ------------------------------------ | ------------------------------------------------------------ | ---- |
| request_type | `string`,支持填入 onebot 标准的内容 | 限定请求事件的类型,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| user_id | `int64``string` | 限定请求事件当事人用户 IDQQ 号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| sub_type | `string` | 限定请求事件来源群 IDQQ 群号),见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| comment | `string` | 限定验证消息内容,见 [OneBot - 请求事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/request.md) | 空 |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQMetaEvent()
元事件元事件不属于用户交互的一部分消息、通知、请求三大类事件是与聊天软件直接相关的、机器人真实接收到的事件除了这些OneBot 自己还会产生一类事件,这里称之为「元事件」,例如生命周期事件、心跳事件等,这类事件与兼容 OneBot 的客户端和炸毛框架本身的运行状态有关,而与聊天软件无关。元事件的上报方式和普通事件完全一样。
### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------- |
| 名称 | `@CQMetaEvent` |
| 触发前提 | 当 `post_type``meta_event` 时触发(元事件上报时) |
| 命名空间 | `ZM\Annotation\CQ\CQMetaEvent` |
| 适用位置 | 方法 |
| 返回值处理 | 无作用 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| --------------- | ------------------ | ------------------------------------------------------------ | ---- |
| meta_event_type | `string`**必需** | 限定元事件的类型,见 [OneBot - 元事件](https://github.com/howmanybots/onebot/blob/master/v11/specs/event/meta.md) | |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
TODO先放着有时间再更。
## CQBefore()
所有机器人事件的前置注解事件,一般用作消息过滤、全局日志、全局替换等。
### 属性
| 类型 | 值 |
| ---------- | ------------------------------------------------------------ |
| 名称 | `@CQBefore` |
| 触发前提 | 当 `post_type` 等于参数 `cq_event` 时触发 |
| 命名空间 | `ZM\Annotation\CQ\CQBefore` |
| 适用位置 | 方法 |
| 返回值处理 | 仅可返回 `bool`,如果为 `false`,则阻断 `cq_event` 类的所有事件防止被执行 |
### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | ------------------------------------------------------------ | ------------------------ | ---- |
| cq_event | `string`**必需**,支持 `message``notice``request``meta_event` | 限定机器人时间的类型 | |
| level | `int` | 事件优先级(越大越靠前) | 20 |
### 用法
代码
```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 :my-chats="[
{type:0,content:'百科 北京'},
{type:1,content:'已搜到匹配 北京 的如下结果blablabla'},
{type:0,content:'百科 谷歌被封'},
{type:2,content:'机器人没有任何回复'},
]"></chat-box>
::: warning 注意
在设置了 `level` 参数后,如果设置了多个 `@CQBefore` 监听事件函数,更高 `level` 的事件函数返回了 `false`,则低 `level` 的绑定函数不会执行,所有 `@CQMessage` 绑定的事件也不会执行。
你也可以使用 `@CQBefore` 做一些消息的转发和过滤。比如你想去除用户发来的文字中的 emoji、图片等 CQ 码,只保留文本。
使用 `ctx()->waitMessage()` 时等待用户输入下一条消息功能和 CQBefore 配合过滤消息时需注意,见 [FAQ - CQBefore 过滤不了 waitMessage](/FAQ/wait-message-cqbefore/)
:::
## CQAfter()
同上。只是在以上所有事件都调用后才会调用的。
## CQAPIResponse()
TODO还没写完先放着有时间再更。

View File

@@ -0,0 +1,237 @@
# 路由注解事件
炸毛框架提供了一个简易但是高效易用的 HTTP 路由注解,你可以使用路由功能来开发任何 Web 应用微服务、API 接口、中间件等。
::: tip 开发提示
本章节涉及的路由和控制器概念可能和其他传统框架有一些出入,而且炸毛框架非绝对根据 PSR 标准进行开发,目的是使用上一些常见的东西尽可能地灵活和不啰嗦。
:::
## 控制器和路由
Controller 和 Route 为路由注解事件的核心注解事件,其中 Controller 的注解事件为 `@Controller`Route 的注解事件为 `@RequestMapping`
### Controller()
#### 属性
| 类型 | 值 |
| ---------- | ------------------------------- |
| 名称 | `@Controller` |
| 触发前提 | 当路由 url 匹配到时进入触发 |
| 命名空间 | `ZM\Annotation\Http\Controller` |
| 适用位置 | 类 |
| 返回值处理 | 对类注解修饰,无返回值 |
#### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------- | -------------- | ------------ | ---- |
| prefix | `string`,必需 | 控制器的 url | 空 |
### RequestMapping()
#### 属性
| 类型 | 值 |
| ---------- | ----------------------------------------------------------- |
| 名称 | `@RequestMapping` |
| 触发前提 | 当路由 url 匹配到时进入触发 |
| 命名空间 | `ZM\Annotation\Http\RequestMapping` |
| 适用位置 | 方法 |
| 返回值处理 | 返回类型是 `string` 时,自动调用 HTTP 响应并返回 200 状态码 |
#### 参数
| 参数名称 | 参数范围 | 用途 | 默认 |
| -------------- | ----------------------------------------------------------- | -------------------------- | ------------------------------------------ |
| route | `string`,必需 | 控制器的 url | 空 |
| name | `string` | 路由的名称 | 空 |
| request_method | `array`,限定 `RequestMethod::GET` 等常量 | 限制激活路由的 HTTP 方法 | `[RequestMethod::GET,RequestMethod::POST]` |
| params | `array`,当路由中含有如 `{id}` 类似的动态路由时,会动态改变 | 动态参数的路由参数值的绑定 | `[]` |
#### 函数调用参数
- `$param`:如果路由中存在变量(动态路由),则会把动态路由所匹配的参数放入 `$param` 数组中。
```php
/**
* @RequestMapping(route="/test/{ass}")
*/
public function testName($param) {
return "Your name is ".($param["ass"] ?? "unknown");
}
```
### 路由示例
代码
```php
<?php
namespace Module\Example;
use ZM\Annotation\Http\Controller;
use ZM\Annotation\Http\RequestMapping;
/**
* @Controller("/api")
*/
class Hello {
/**
* @RequestMapping("/index")
*/
public function index(){
ctx()->getResponse()->end("This is API index page"); // 使用上下文获取响应对象
}
/**
* @RequestMapping("/ping")
*/
public function ping(){
return "pong"; // 直接返回字符串
}
}
```
效果
::: tip 效果描述
当访问浏览器的 `http://localhost:20001/api/index` 时,浏览器会返回 `This is API index page`,当访问 `/api/ping` 的 url 时,浏览器会返回 `pong`
```
/ -> 无任何路由
/api/index -> Hello->index
/api/ping -> Hello->ping
```
:::
::: tip 提示
`@Controller``/` 的时候,效果和不写是一样的,`@RequestMapping``/``/index/inside` 等多级路由也是可以的。
:::
### 绑定参数
`@RequestMapping` 中,不仅可以写静态的路由地址,也可以写绑定的参数。例如:`@RequestMapping(route="/index/{name}")`,则访问 `/index/xxx` 的时候,你在函数方法内可以这样获取此参数:
```php
/**
* @RequestMapping("/index/{name}")
*/
public function index($arg) {
return "Your param 'name' is ".$arg["name"];
}
```
## 获取请求参数 GET / POST
炸毛框架支持获取外部 HTTP 请求进来的 GET 和 POST 请求,通过获取 HTTP 请求对象 [Request](/advanced/inside-class/) 即可。对象具体属性和方法点这个链接进去就行。
### 示例
#### 获取 GET
```php
/**
* @RequestMapping("/testUrl")
*/
public function testUrl() {
$get = ctx()->getRequest()->get;
if(isset($get["name"])) return "hello, ".$get["name"];
else return "Unknown name!!";
}
```
#### 获取 POSTx-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!!";
}
```
#### 获取 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!!";
}
```
## 设置路由请求方式
如果想要设置允许请求控制器的 HTTP 请求方式,可以使用方法在控制器中的 `@RequestMapping` 注解配置 `method` 参数,可以是 `GET``POST``PUT`, `PATCH``DELETE``OPTIONS``HEAD` 中的一个或多个。
- 限定 HTTP 方法:`@RequestMapping(method="GET")``@RequestMapping(method={"GET","POST"})`
## 静态文件服务器
框架支持了静态文件的访问。如需使用,则需要先到配置文件中配置相应的 `static_file_server` 参数中 `status``true`
框架分为两种静态文件服务器,一种是全局的静态文件服务器,比如框架部署在 `http://127.0.0.1:20001/` 上通过 HTTP 访问,如果没有访问到 `@RequestMapping` 注解事件注册的路由地址,则会通过 url 自动查找静态文件服务器设置的根路径下面的文件,如果都不存在则会返回 404。
### 配置全局静态文件服务器
我们假设在你写的框架应用的根目录下,有如下文件和内容:
```
resources/html/hello.html (下面是内容)
<html>
<head>
<meta charset="utf-8">
</head>
<body>
框架文档内容太多了,写不完!!!
</body>
</html>
```
然后在 `global.php` 配置文件中静态文件服务器参数为:
```php
/** 静态文件访问 */
$config['static_file_server'] = [
'status' => true,
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
'document_index' => [
'index.html'
]
];
```
最终,我们通过 `vendor/bin/start server` 等方式,启动框架后,浏览器访问 `http://127.0.0.1:20001/hello.html` 即可获取内容。
### 配置局部静态文件服务器
所涉及的类的命名空间:`use ZM\Http\StaticFileHandler;`
局部静态文件服务器一般用于,比如机器人要发送图片,或者给其他 HTTP 服务提供文件下载的接口时可用。我们假设写了一个图片收集的一个静态文件夹区域,将其中一个子路由当作图片静态目录:
```php
/**
* @RequestMapping("/images/{filename}")
* @param $param
* @return StaticFileHandler
*/
public function staticImage($param) {
Console::info("[下载图片] " . $param["filename"]);
return new StaticFileHandler($param["filename"], "/path/to/your/image_dir/");
}
```
这样当用户访问 `http://框架地址/images/aaa.jpg` 就可以快速地调用此路由下的局部文件服务器功能了。

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

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

View File

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

View File

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

View File

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

View File

@@ -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")` 了。

View 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`
![image-20210708003903652](https://static.zhamao.me/images/docs/image-20210708003903652.png)
其中,`-zsh` 下有唯一一个 php 进程,在图中对应的第一列 pid 为 `16258`,代表 Master 进程。
Master 进程下的唯一一个子进程(白色的是进程,绿色是线程),在图中对应的 pid 为 `16263`,代表 Manager 进程,用作管理 Worker 进程。
Manager 进程下的子进程,连号部分为对应的 Worker 进程,比如图中的 `16266``16267``16268``16269` 分别代表 `Worker #0``Worker #1``Worker #2``Worker #3` 四个 Worker 进程。
如果你还设置了 TaskWorker 进程TaskWorker 进程的 pid 会和 Worker 进程一样是连续的,一般会接在 Worker 进程后面。
`htop` 使用方向键选择进程,选择到对应进程后可以使用 `F9` 来选择 kill 指令,比如让框架热重启,可以将光标移到 Master 进程上,使用 `SIGUSR1`
![image-20210708004921655](https://static.zhamao.me/images/docs/image-20210708004921655.png)

View File

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

50
docs/guide/README.md Normal file
View File

@@ -0,0 +1,50 @@
# 介绍
::: tip 提示
编写文档需要较大精力,你也可以参与到本文档的建设中来,比如找错字,增加或更正内容,每页文档可直接点击右上方铅笔图标直接跳转至 GitHub 进行编辑,编辑后自动 Fork 并生成 Pull Request以此来贡献此文档
:::
炸毛框架使用 PHP 编写,采用 Swoole 扩展为基础,主要面向 API 服务聊天机器人OneBot 标准的机器人对接),包含 WebSocket、HTTP 等监听和请求库,用户代码采用模块化处理,使用注解可以方便地编写各类功能。
框架主要用途为 HTTP/WebSocket 服务器,机器人搭建框架。尤其对于聊天机器人消息处理较为方便和全面,提供了众多会话机制和内部调用机制,可以以各种方式设计你自己的模块。
此外QQ 机器人方面此框架基于 OneBot 标准的反向 WebSocket 连接,比传统 HTTP 通信更快。
```php
/**
* @CQCommand("你好")
*/
public function hello() {
ctx()->reply("你好,我是炸毛!");
}
/**
* @RequestMapping("/index")
*/
public function index() {
return "<h1>hello!</h1>";
}
```
## 开始前
首先,你需要了解你需要知道哪些事情才能开始着手使用框架:
1. Linux 命令行(会跑 Linux 程序)
2. php >=7.2 开发环境(项目会持续支持最新的 PHP 版本)
4. OneBot 机器人聊天接口标准
需要值得注意的是,本教程中所涉及的内容均为尽可能翻译为白话的方式进行描述,但对于框架的组件或事件等需要单独拆分说明文档的部分则需要足够详细,所以本教程提供一个快速上手的教程,并且会将最典型的安装方式写到快速教程篇。
## 框架特色
- 支持MySQL数据库连接池自带查询缓存提高多查询时的效率
- Websocket 服务器、HTTP 服务器兼容运行,一个框架多个用处
- 支持命令、自然语言处理等多种插件形式
- 支持多个机器人账号负载均衡
- 协程 + TaskWorker 进程重度任务处理机制,保证高效,单个请求响应时间为 0.1 ms 左右
- 模块分离和自由组合,可根据自身需求自己建立模块内的目录结构和代码结构
- 灵活的注释注解注册事件方式,弥补 PHP 语言缺少注解的遗憾

195
docs/guide/basic-config.md Normal file
View File

@@ -0,0 +1,195 @@
# 基本配置
到目前为止,炸毛框架的配置文件还没有任何变更,是默认的行为。在本章内容中,将列举出炸毛框架的配置文件的规则和使用。
::: danger 警告
因为炸毛框架的全局配置中含有数据库名称和密码以及 access_token 等敏感字段,在使用版本控制软件过程中请不要将敏感信息写入配置文件并提交至开源仓库!
:::
## 全局配置文件 global.php
框架的全局配置文件在 `config/global.php` 文件中。下面是配置文件的各个选项,请根据自己的需要自行配置。
| 配置名称 | 说明 | 默认值 |
| :--------------------------- | ------------------------------------------------------------ | ---------------------------- |
| `host` | 框架监听的地址 | 0.0.0.0 |
| `port` | 框架监听的端口 | 20001 |
| `http_reverse_link` | 框架开到公网或外部的 HTTP 反代链接 | 见配置文件 |
| `zm_data` | 框架的配置文件、日志文件等文件目录 | `./` 下的 `zm_data/` |
| `debug_mode` | 框架是否启动 debug 模式 | false |
| `crash_dir` | 存放崩溃和运行日志的目录 | `zm_data` 下的 `crash/` |
| `config_dir` | 存放 saveToJson() 方法保存的数据的目录 | `zm_data` 下的 `config/` |
| `swoole` | 对应 Swoole server 中 set 的参数参考Swoole文档 | 见子表 `swoole` |
| `runtime` | 一些框架运行时调整的设置 | 见子表 `runtime` |
| `light_cache` | 轻量内置 key-value 缓存 | 见字表 `light_cache` |
| `worker_cache` | 跨进程变量级缓存 | 见子表 `worker_cache` |
| `mysql_config` | MySQL 数据库连接信息 | 见子表 `mysql_config` |
| `redis_config` | Redis 连接信息 | 见子表 `redis_config` |
| `access_token` | OneBot 客户端连接约定的token留空则无相关设置见 [组件 - Access Token 验证](component/access-token) | 空 |
| `http_header` | HTTP 请求自定义返回的header | 见配置文件 |
| `http_default_code_page` | HTTP服务器在指定状态码下回复的默认页面 | 见配置文件 |
| `init_atomics` | 框架启动时初始化的原子计数器列表 | 见配置文件 |
| `info_level` | 终端日志显示等级0-4 | 2 |
| `context_class` | 上下文所定义的类,见对应上下文说明文档 | `\ZM\Context\Context::class` |
| `static_file_server` | 静态文件服务器配置项 | 见子表 `static_file_server` |
| `server_event_handler_class` | 注册 Swoole Server 事件注解的类列表,在 Swoole 服务器启动前就被加载 | 空 |
| `onebot` | OneBot 协议相关配置 | 见子表 `onebot` |
| `remote_terminal` | 远程终端相关配置 | 见子表 `remote_terminal` |
| `module_loader` | 模块/插件 加载相关配置 | 见子表 `module_loader` |
### 子表 **swoole**
| 配置名称 | 说明 | 默认值 |
| ----------------------- | ------------------------------------------------------------ | ----------------------------------- |
| `log_file` | Swoole 的日志文件 | `crash_dir` 下的 `swoole_error.log` |
| `worker_num` | Worker 工作进程数 | 运行框架的主机 CPU 核心数 |
| `dispatch_mode` | 数据包分发策略,见 [文档](https://wiki.swoole.com/#/server/setting?id=dispatch_mode) | 2 |
| `max_coroutine` | 最大协程并发数 | 300000 |
| `max_wait_time` | 退出进程时等待协程恢复的最长时间(秒) | 52.4.3 版本后默认值) |
| `task_worker_num` | TaskWorker 工作进程数 | 默认不开启(此参数被注释) |
| `task_enable_coroutine` | TaskWorker 工作进程启用协程 | 默认不开启(此参数被注释)或 `bool` |
### 子表 runtime
| 配置名称 | 说明 | 默认值 |
| ----------------------------- | ------------------------------------------------------------ | --------------------------------------- |
| `swoole_coroutine_hook_flags` | Swoole 启动时一键协程化 Hook 的 Flag 值,详见 [一键协程化](http://wiki.swoole.com/#/runtime?id=%e5%87%bd%e6%95%b0%e5%8e%9f%e5%9e%8b) | `SWOOLE_HOOK_ALL & (~SWOOLE_HOOK_CURL)` |
| `swoole_server_mode` | Swoole Server 启动的进程模式,有 `SWOOLE_PROCESS``SWOOLE_BASE` 两种,见 [启动方式](http://wiki.swoole.com/#/learn?id=swoole_process) | `SWOOLE_PROCESS` |
| `middleware_error_policy` | 中间件错误处理策略,见 [中间件 - 错误处理策略](../../event/middleware/#_6) | 1 |
| `reload_delay_time` | 框架 reload 重载命令接收后延迟的时间毫秒0 为不等待) | 800 |
| `global_middleware_binding` | 给注解事件绑定全局中间件,见 [中间件 - 全局中间件](../../event/middleware/#_6) | `[]` |
| `save_console_log_file` | 当这里输入字符串路径时,所有 `Console::xxx()` 输出的日志都将保存到目标文件 | false |
### 子表 **light_cache**
| 配置名称 | 说明 | 默认值 |
| -------------------------- | ----------------------------------------------- | ---------------------------- |
| `size` | 最多可以缓存的 k-v 条目数(必须是 2 的 n 次方) | 512 |
| `max_strlen` | 作为 value 字符串的最大长度 | 32768 |
| `hash_conflict_proportion` | Hash冲突率越大越好但是需要的内存更多 | 0.6 |
| `persistence_path` | 持久化的键值对的存储路径 | `zm_data` 下的 `_cache.json` |
| `auto_save_interval` | 持久化的键值对自动保存时间间隔(秒) | 900 |
### 子表 worker_cache
| 配置名称 | 说明 | 默认值 |
| -------- | --------------------------- | ------ |
| `worker` | 跨进程缓存的存储工作进程 id | 0 |
### 子表 **mysql_config**
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------ | ------------------------------------------------------------ |
| `host` | 数据库地址(留空则不使用数据库) | 空 |
| `port` | 数据库端口 | 3306 |
| `username` | 连接数据库的用户名 | |
| `dbname` | 要连接的数据库名 | |
| `password` | 数据库连接密码 | |
| `options` | PDO 数据库的 options 参数 | `[PDO::ATTR_STRINGIFY_FETCHES => false,PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]` |
| `pool_size` | MySQL 连接池大小 | 64 |
| `charset` | MySQL 字符集 | `utf8mb4` |
### 子表 **redis_config**
| 配置名称 | 说明 | 默认值 |
| ---------- | ------------------------------------------ | ------ |
| `host` | Redis 服务器地址,留空则启动时不创建连接池 | 空 |
| `port` | Redis 服务器端口 | 6379 |
| `timeout` | Redis 超时时间 | 1 |
| `db_index` | Redis 要连接的数据库 index | 0 |
| `auth` | 认证字符串 | 空 |
### 子表 static_file_server
| 配置名称 | 说明 | 默认值 |
| ---------------- | ---------------------- | ------------------------------ |
| `status` | 是否开启静态文件服务器 | false |
| `document_root` | 静态文件的根目录 | `{WORKING_DIR}/resources/html` |
| `document_index` | 默认索引的文件名列表 | `["index.html"]` |
### 子表 onebot
| 配置名称 | 说明 | 默认值 |
| ------------------------ | ------------------------------------------------------------ | ------ |
| `status` | 是否开启 OneBot 标准机器人解析功能 | true |
| `single_bot_mode` | 是否开启单机器人模式 | false |
| `message_level` | 机器人的 WebSocket 事件在 Swoole 原生事件 `@OnMessageEvent` 中的等级(越高说明越被优先处理) | 99 |
| `message_convert_string` | 是否将数组格式的消息转换为字符串以保证与旧版本的兼容性 | true |
| `message_command_policy` | CQCommand命令匹配后执行流程`interrupt` 为不执行后续 CQMessage`continue` 为继续 | `interrupt` |
### 子表 remote_terminal
| 配置名称 | 说明 | 默认值 |
| -------- | ------------------------------------------------------------ | ----------- |
| `status` | 是否开启远程终端功能,见 [组件 - 远程终端](/component/remote-terminal) | false |
| `host` | 远程终端监听地址为安全起见默认值只允许本地回环地址127.0.0.1 | `127.0.0.1` |
| `port` | 远程终端监听的 TCP 端口 | 20002 |
| `token` | 远程终端连接的令牌(如果为空("")则不验证) | "" |
### 子表 module_loader
| 配置名称 | 说明 | 默认值 |
| -------- | ------------------------------------------------------------ | ----------- |
| `enable_hotload` | 是否开启热加载模块包的功能 | false |
| `load_path` | 模块包加载的目录地址 | `zm_data` 下的 `modules` |
## 多环境下的配置文件
炸毛框架的配置文件模块支持不同环境下的配置文件,主要结构为 `global.{环境}.php`。在一般情况下,炸毛框架默认从教程引导方式根据指令 `vendor/bin/start server` 启动的框架是不带环境控制的。这章将讲述如何根据不同的环境development / staging / production来编写配置文件。
### 使用环境参数
在启动框架时,额外增加参数 `--env` 可以指定当前的环境,从而使用不同的配置文件。现在框架支持以下几种环境: `development``staging``production`
```bash
vendor/bin/start server --env=development
```
### 不同环境配置文件
由于框架默认只带有 `global.php` 文件,所以假设你现在需要区分开发环境和生产环境的配置,将 `global.php` 文件复制并重命名为 `global.development.php``global.production.php` 即可。
### 优先级
如果指定了 `--env` 环境参数:`global.{对应环境}.php` > `global.php`,如果两个配置文件都找不到则报错。
如果未指定 `--env` 环境参数:`global.php` > `global.development.php` > `global.staging.php` > `global.production.php`
## 其他自定义配置文件
炸毛框架的全局配置文件为 `global.php`,为了让不同的开发者更好的二次开发或者集成更多功能,炸毛框架的配置文件模块也支持自己编写的其他 `*.php``*.json` 格式的配置文件。例如炸毛框架默认附带了 `file_header.json` 这个配置文件(用来返回各类文件扩展名对应的 `Content-Type` 头参数的表)。
使用也非常简单,我们先以 `.json` 格式为例,我们创建一个 `example_a.json` 文件在 `config/` 目录(和 `global.php` 一个文件夹下),并编写自己的任意配置内容:
```json
{
"key1": "value1"
}
```
在框架中,启动后就会默认加载,使用只需要用以下方式即可:
```php
use ZM\Config\ZMConfig; # 先 use 再使用!
$r = ZMConfig::get("example_a", "key1"); # $r == "value1"
```
如果需要用到变量或其他动态的内容,可以使用 `.php` 格式的配置文件。这里还是以 `example_a.php` 来举例:
```php
<?php
$config['key1'] = "value1";
$config['starttime'] = time();
return $config;
```
使用方式同上:
```php
$r = ZMConfig::get("example_a", "key1"); # $r == "value1"
$time = ZMConfig::get("example_a", "starttime"); # $time == 服务器启动时间
```
同时,自定义配置文件也支持环境变量,例如:`example_a.development.json``example_a.production.php` 均可。

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

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

View File

@@ -0,0 +1,82 @@
# 安装
> 这篇为炸毛框架以及环境的部署教程。
框架部署分为两部分,一部分是安装 PHP 环境,另一部分是通过 Composer 或 GitHub 拉取框架的脚手架。
## 一键下载静态 PHP 环境和框架脚手架
从 2.4.4 版本起,炸毛框架支持一键拉取一个静态的 PHP 运行时和脚手架,只需运行下面的脚本即可。(开发环境推荐此方法)
```bash
# 将会把 PHP、框架都安装在此目录下
mkdir zhamao-app/ # 这里可以取自己的项目名字
cd zhamao-app/
bash -c "$(curl -fsSL https://api.zhamao.xin/go.sh)"
# 安装完成后的启动框架命令2.5.0 版本后可省略掉 runtime/php 前缀)
vendor/bin/start server
# 扩展用法:使用静态 PHP 版本的 Composer update
runtime/composer update
# 扩展用法:使用静态 PHP 运行别的 CLI 脚本
runtime/php path/to/your/script.php
```
> 有关静态 PHP 的多种用法(如 Composer见 [进阶 - PHP 环境高级](/advanced/php-env)
## 使用 Docker 部署 PHP 和框架
你也可以使用 Docker 进行拉取 PHP 环境。
```bash
# 拉取 Docker 镜像
docker pull zmbot/swoole
# 再通过 GitHub 或其他方式拉取框架脚手架
git clone --depth=1 https://github.com/zhamao-robot/zhamao-framework-starter.git
cd zhamao-framework-starter/
# Docker 内使用 Composer 更新依赖
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole composer update
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start init
# 使用 Docker 启动框架
docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole vendor/bin/start server
```
启动后你会看到和下方类似的初始化内容,表明启动成功了
```verilog
$ vendor/bin/start server
=================================================================
working_dir: /app/zhamao-framework-starter
listen: 0.0.0.0:20001 | worker: 4 (auto)
environment: default | log_level: 2
version: 2.7.0 | master_pid: 28449
=================================================================
______
|__ / |__ __ _ _ __ ___ __ _ ___
/ /| '_ \ / _` | '_ ` _ \ / _` |/ _ \
/ /_| | | | (_| | | | | | | (_| | (_) |
/____|_| |_|\__,_|_| |_| |_|\__,_|\___/
[03-20 22:30:56] [S] [#1] Worker #1 started
[03-20 22:30:56] [S] [#2] Worker #2 started
[03-20 22:30:56] [S] [#3] Worker #3 started
[03-20 22:30:56] [S] [#0] Worker #0 started
```
单纯运行 炸毛框架 后,如果不部署或安装启动任何机器人客户端的话,仅仅相当于启动了一个 监听 20001 端口的WebSoket + HTTP 服务器。你可以通过浏览器访问http://127.0.0.1:20001 ,或者你部署到了服务器后需要输入服务器地址。
## 命令总结
1. 对于框架的启动,必须 cd 到项目的跟目录,比如 `cd zhamao-app/` 进入到项目根目录。
2. 无论何种方式启动,启动框架的命令格式都为这个格式:`{php二进制路径} vendor/bin/start server {--如果需要参数的话这样跟}`
3. 第二条的 `php 二进制路径` 指的是,比如使用第一种静态 PHP 环境,这里写 `runtime/php` 就好了,如果是安装到系统的 PHP 的话,这里为空,如果是 Docker 部署的环境,则这里填 `docker run -it --rm -v $(pwd):/app/ -p 20001:20001 zmbot/swoole`
## 使用 IDE 等工具开发代码
我们使用文本编辑器进行炸毛框架开发,在使用集成开发环境 **IDEA****PhpStorm** 时,推荐通过插件市场搜索并安装 **PHP Annotations** 插件以提供注解命名空间自动补全、注解属性代码提醒、注解类跳转等,非常有助于提升开发效率的功能。
## 进阶环境部署和开发
炸毛框架还支持更多种启动方式,如源码模式、守护进程模式,具体后续有关环境和部署的进阶教程,请查看 [进阶开发](/advanced/) 部分!

View File

@@ -2,7 +2,7 @@
## 什么是 OneBot
OneBot 是一个聊天机器人应用接口标准,详情戳[这里](https://github.com/howmanybots/onebot)。
OneBot 是一个聊天机器人应用接口标准,详情戳 [这里](https://github.com/howmanybots/onebot)。
## OneBot 实现选择
@@ -16,8 +16,10 @@ OneBot 是一个聊天机器人应用接口标准,详情戳[这里](https://gi
| [takayama-lily/onebot](https://github.com/takayama-lily/onebot) | [OICQ](https://github.com/takayama-lily/oicq) | takayama | |
| [ProtobufBot](https://github.com/ProtobufBot) | [Mirai](https://github.com/mamoe/mirai) | lz1998 | 事件和 API 数据内容和 OneBot 一致,通信方式不兼容 |
!!! warning "注意"
::: warning 注意
因为目前炸毛框架 2.0 只支持 WebSocket 方式的 OneBot 实现,所以目前上述项目的连接方式均只可选支持反向 WebSocket 通信的。后期会兼容 HTTP 和正向 WebSocket 通信方式。
因为目前炸毛框架 2.0 只支持 WebSocket 方式的 OneBot 实现,所以目前上述项目的连接方式均只可选支持反向 WebSocket 通信的。后期会兼容 HTTP 和正向 WebSocket 通信方式。
如果你还没有自己的 QQ或者是其他原因导致的暂时无法使用上述 OneBot 实例,可以使用炸毛项目中的 OneBot 协议聊天模拟器。但目前还处在开发中,暂不可用。
:::
如果你还没有自己的 QQ或者是其他原因导致的暂时无法使用上述 OneBot 实例,可以使用炸毛项目中的 OneBot 协议聊天模拟器。但目前还处在开发中,暂不可用。

View File

@@ -0,0 +1,9 @@
# 快速上手 - HTTP 服务器篇
HTTP 服务器篇主要讲解如何通过炸毛框架来实现微服务、API 通用接口等等这些东西的。
- [HTTP 服务器 - 路由和静态文件篇](/event/route-annotations/)
- [HTTP 服务器 - 存储 - LightCache 轻量缓存](/component/light-cache/)
- [HTTP 服务器 - 存储 - Redis](/component/redis/)
- [HTTP 服务器 - 存储 - MySQL](/component/mysql/)
- [HTTP 客户端](/component/zmrequest/)

View File

@@ -0,0 +1,175 @@
# 快速上手 - 机器人篇
## 简介
看到这里,你已经完成了前面的环境部署,到了最关键的第一步了!
一切都安装成功后,你就已经做好了进行简单配置以运行一个最小的 **机器人问答模块** 的准备。
炸毛框架和机器人客户端是什么关系呢?炸毛框架就好比我们传统的一系列例如 Spring 框架、ThinkPHP 框架等,是服务端,而机器人客户端是一个 HTTP / WebSocket 客户端,时刻准备着连接到炸毛框架。
## 机器人客户端
开发之前请注意,**机器人客户端和框架相互独立!**故有关**机器人客户端**出现的问题请到对应机器人客户端开发者或 GitHub 项目中咨询和讨论,炸毛框架为对接机器人客户端的一个快速开发的框架。
机器人客户端是炸毛框架以外的程序或软件,目前炸毛框架支持的机器人客户端通信标准为 OneBot 标准(原 CQHTTP只要你的机器人客户端是 OneBot 标准的,就可以和炸毛框架进行无缝对接。
OneBot 机器人部分的选择详情见 [OneBot 实例](/guide/OneBot实例/)。
这里以炸毛框架开发过程中使用的 [go-cqhttp](https://github.com/Mrs4s/go-cqhttp) 来举例进行第一个机器人的配置工作。
简要步骤描述为:
1. 下载 go-cqhttp 对应平台的 [release 文件](https://github.com/Mrs4s/go-cqhttp/releases)
2. 双击 exe 文件或者使用 `./go-cqhttp` 启动
3. 生成默认配置文件并修改默认配置
::: warning 注意
由于 go-cqhttp 项目还处于开发期,而且配置文件格式也发生了多次变化,但大体内容没有变(比如编写此文档时发布的版本中配置文件格式变成了 `hjson` 取代了原来的 `json`
:::
config.yml最新格式
```yaml {2,3,78,83}
account: # 账号相关
uin: 1233456 # QQ账号
password: '' # 密码为空时使用扫码登录
encrypt: false # 是否开启密码加密
status: 0 # 在线状态 请参考 https://github.com/Mrs4s/go-cqhttp/blob/dev/docs/config.md#在线状态
relogin: # 重连设置
delay: 3 # 首次重连延迟, 单位秒
interval: 3 # 重连间隔
max-times: 0 # 最大重连次数, 0为无限制
# 是否使用服务器下发的新地址进行重连
# 注意, 此设置可能导致在海外服务器上连接情况更差
use-sso-address: true
heartbeat:
# 心跳频率, 单位秒
# -1 为关闭心跳
interval: 5
message:
# 上报数据类型
# 可选: string,array
post-format: string
# 是否忽略无效的CQ码, 如果为假将原样发送
ignore-invalid-cqcode: false
# 是否强制分片发送消息
# 分片发送将会带来更快的速度
# 但是兼容性会有些问题
force-fragment: false
# 是否将url分片发送
fix-url: false
# 下载图片等请求网络代理
proxy-rewrite: ''
# 是否上报自身消息
report-self-message: false
# 移除服务端的Reply附带的At
remove-reply-at: false
# 为Reply附加更多信息
extra-reply-data: false
output:
# 日志等级 trace,debug,info,warn,error
log-level: warn
# 是否启用 DEBUG
debug: false # 开启调试模式
# 默认中间件锚点
default-middlewares: &default
# 访问密钥, 强烈推荐在公网的服务器设置
access-token: ''
# 事件过滤器文件目录
filter: ''
# API限速设置
# 该设置为全局生效
# 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
# 目前该限速设置为令牌桶算法, 请参考:
# https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
rate-limit:
enabled: false # 是否启用限速
frequency: 1 # 令牌回复频率, 单位秒
bucket: 1 # 令牌桶大小
database: # 数据库相关设置
leveldb:
# 是否启用内置leveldb数据库
# 启用将会增加10-20MB的内存占用和一定的磁盘空间
# 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
enable: true
# 连接服务列表
servers:
# 添加方式,同一连接方式可添加多个,具体配置说明请查看文档
#- http: # http 通信
#- ws: # 正向 Websocket
#- ws-reverse: # 反向 Websocket
#- pprof: #性能分析服务器
# Zhamao Framework 所需要的服务器配置
- ws-reverse:
# 是否禁用当前反向WS服务
disabled: false
# 反向WS Universal 地址
# 注意 设置了此项地址后下面两项将会被忽略
universal: ws://127.0.0.1:20001/
# 反向WS API 地址
api: ws://your_websocket_api.server
# 反向WS Event 地址
event: ws://your_websocket_event.server
# 重连间隔 单位毫秒
reconnect-interval: 3000
middlewares:
<<: *default # 引用默认中间件
```
其中 ws://127.0.0.1:20001/ 中的 127.0.0.1 和 20001 应分别对应炸毛框架配置的 HOST 和 PORT
## 第一次对话
一旦新的配置文件正确生效之后,所在的控制台(如果正在运行的话)应该会输出类似下面的内容:
```verilog
[15:26:34] [I] [#2] 机器人 你的QQ号 已连接!
```
表明机器人已成功连接到炸毛框架了!
这时,如果你是根据安装教程走下来并且未编写任何模块,炸毛自带一个示例模块,里面含有命令:`你好``随机数`。如果你对机器人回复:`你好`,它会回复你 `你好啊,我是由炸毛框架构建的机器人!`。这一历史性的对话标志着你已经成功地运行了炸毛框架,开始了编写更强大的 QQ 机器人的创意之旅!
## 编写一个命令
让我们转到框架的模块源代码部分,目录是 `src/Module/Example`,文件是 `Hello.php`。我们插入一段这样的代码:
```php
/**
* @CQCommand("echo")
*/
public function repeat() {
$repeat = ctx()->getFullArg("请输入你要回复的内容");
ctx()->reply($repeat);
//return $repeat; // 这样的效果等同于 ctx()->reply()
}
```
这样,一个简易的复读机就做好了!回到 QQ 机器人聊天,向机器人发送 `echo 你好啊`,它会回复你 `你好啊`。
<chat-box :my-chats="[
{type:0,content:'echo 你好啊'},
{type:1,content:'你好啊'},
{type:0,content:'echo'},
{type:1,content:'请输入你要回复的内容'},
{type:0,content:'哦豁'},
{type:1,content:'哦豁'},
]"></chat-box>
> 如果你只回复 `echo` 的话,它会先和你进入一个会话状态,并问你 `请输入你要回复的内容`,这时你再次说一些内容例如 `哦豁`,会回复你 `哦豁`。效果和直接输入 `echo 哦豁` 是一致的,这是炸毛框架内的一个封装好的命令参数对话询问功能。有关参数询问功能,请看后面的进阶模块。
## 使用机器人 API 和事件
如果你想不只是回复消息还要做其他复杂的动作Action使用 OneBot Action又名 OneBot API进行发送即可见 [机器人 API](/component/bot/robot-api)。
如果想处理其他类型的事件,比如 QQ 群通知事件等,见 [机器人注解事件](/event/robot-annotations/)。

View File

@@ -39,8 +39,11 @@ class Weather {
}
```
!!! note "提示"
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API本教程的进阶一栏后期会详细编写如何对接一个天气 API 接口。
::: tip 提示
为了简单起见,我们在这里的例子中没有接入真实的天气数据,但要接入也非常简单,你可以使用中国天气网、和风天气等网站提供的 API本教程的进阶一栏后期会详细编写如何对接一个天气 API 接口。
:::
在上方代码编写完毕后,运行框架(或运行过程中终端输入 `reload`),然后使用机器人客户端连接到炸毛框架,即可实现我们的第一个功能。我们在代码中编写了一个**注解事件**`@CQCommand`,此注解事件是用于接收用户的普通消息并切分成各类命令规则的一个注解事件绑定。代码中的注解事件省去了注解中的键名,也可以写作 `@CQCommand(match="天气")`
@@ -56,11 +59,11 @@ class Weather {
最后,函数直接返回了一个字符串,作为事件的响应,炸毛框架会自动处理并调用机器人客户端的接口,最后返回给用户消息。这里也可以使用上下文的 `ctx()->reply("xxx")` 方法替代,不返回字符串。注意两者只能选择一种方式,取决于开发者的开发习惯。
<chat-box>
) 天气 北京
( 北京 天气情况2020年12月22日-2~9℃ blablabla
) 天气
( 请告诉我你要查询的城市
) 北京
( 北京 天气情况2020年12月22日-2~9℃ blablabla
</chat-box>
<chat-box :my-chats="[
{type:0,content:'天气 北京'},
{type:1,content:'北京 天气情况2020年12月22日-2~9℃ blablabla'},
{type:0,content:'天气'},
{type:1,content:'请告诉我你要查询的城市'},
{type:0,content:'北京'},
{type:1,content:'北京 天气情况2020年12月22日-2~9℃ blablabla'},
]"></chat-box>

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

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

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