mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-10-07 11:16:43 +00:00
Compare commits
257 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
5e86d53e0b | ||
|
a50a3398de | ||
|
950930a275 | ||
|
c7af169a94 | ||
|
e17096d8d7 | ||
|
00e9e74dfc | ||
|
934954d985 | ||
|
a4a4991473 | ||
|
60acb71033 | ||
|
f8b4dfb1b1 | ||
|
ee5561046f | ||
|
6660c3b471 | ||
|
bd5ba84737 | ||
|
15c5464069 | ||
|
7b136548a9 | ||
|
36ed8030d3 | ||
|
eff1fe455f | ||
|
e3cb4c7907 | ||
|
be732cf9d8 | ||
|
88a5966a40 | ||
|
bdde496332 | ||
|
a989a895e4 | ||
|
7fc51e9227 | ||
|
571fd007ba | ||
|
599ef3b253 | ||
|
c0c7d141ef | ||
|
3be68895e5 | ||
|
ff21ceb946 | ||
|
bd9befbb55 | ||
|
ed91ec9bf5 | ||
|
a00def5d86 | ||
|
973282587e | ||
|
8a6d209942 | ||
|
fcd226031b | ||
|
fb3f0d5e30 | ||
|
56518748d9 | ||
|
474398ee3d | ||
|
597b104111 | ||
|
acec8945ac | ||
|
06b5f09371 | ||
|
15470cd3bb | ||
|
c1c5f57e0b | ||
|
533e8794b2 | ||
|
05c20a7a86 | ||
|
edb416736b | ||
|
2a68bb1b6e | ||
|
29ffbc630a | ||
|
5cf6b93984 | ||
|
30011e3fb4 | ||
|
36606ab05a | ||
|
0aba6b4bb4 | ||
|
fab51d9605 | ||
|
d7e2cc608b | ||
|
b2c5ab3235 | ||
|
1668568d1a | ||
|
4385934a6b | ||
|
4830182050 | ||
|
d86a86d4b2 | ||
|
f175bc9e80 | ||
|
40c2bc636a | ||
|
8ad5a8d4d1 | ||
|
aed91dcc48 | ||
|
de8ffb6c97 | ||
|
990cf32304 | ||
|
09b3f13e7e | ||
|
c39b13b782 | ||
|
3ec4611a29 | ||
|
9d6832303d | ||
|
9fc9f7c384 | ||
|
2021e81ed2 | ||
|
ada6e1ab64 | ||
|
2723a372da | ||
|
a56c93cbcc | ||
|
230476d8ae | ||
|
31a13551be | ||
|
82dbacda83 | ||
|
badd53b4bb | ||
|
94052b5bf7 | ||
|
72a6914980 | ||
|
a2a604dd85 | ||
|
d3d0779d30 | ||
|
a45e7d3854 | ||
|
4dadef3e51 | ||
|
ee643544f1 | ||
|
4598a3de9a | ||
|
59ad3d4b17 | ||
|
ba78c3aef8 | ||
|
3ce2b69431 | ||
|
d47722d87c | ||
|
c4ddfc3df1 | ||
|
58c8879cbb | ||
|
eb1342b78d | ||
|
feb619a85c | ||
|
a9ec70e798 | ||
|
c96e9dbcb6 | ||
|
8742d867e8 | ||
|
ed14dcd090 | ||
|
7956b53530 | ||
|
1140d668b6 | ||
|
9351b074b1 | ||
|
61dc206935 | ||
|
70f62bf4da | ||
|
812c0cd624 | ||
|
5107729290 | ||
|
9b59c16b04 | ||
|
fdc1dcace7 | ||
|
c6c22e3c29 | ||
|
0291b10560 | ||
|
6f07ce0060 | ||
|
b375575792 | ||
|
88765711f3 | ||
|
17ba8d70e1 | ||
|
e373251092 | ||
|
63dcc658da | ||
|
3aaf86e9a9 | ||
|
2cabeb658e | ||
|
0c187cd8c3 | ||
|
c0c8e1aa02 | ||
|
a8586d7990 | ||
|
91b3d3d5e0 | ||
|
8ef51154fd | ||
|
86c83064e4 | ||
|
a4be2c465f | ||
|
be4f36036c | ||
|
f6c7fb6da6 | ||
|
c9b4c3f3c0 | ||
|
4af4412cd7 | ||
|
4e7f7fb722 | ||
|
f01f692fde | ||
|
6e94dade69 | ||
|
eaef8dfc19 | ||
|
dec8b26b89 | ||
|
e8c39f9cc8 | ||
|
2a8644de81 | ||
|
5aaa0d3f12 | ||
|
22bb377fcf | ||
|
0c0ad0dd5e | ||
|
c947bdfef5 | ||
|
b197802d9a | ||
|
8019a570cc | ||
|
25a85330a7 | ||
|
35fb4fc18d | ||
|
ff50e997d0 | ||
|
6cda981aa2 | ||
|
dcfbb32363 | ||
|
d8a1a0ab38 | ||
|
6efd01a575 | ||
|
b2f7846eb4 | ||
|
8d2a284fe2 | ||
|
cce13f682d | ||
|
6c1d7ad74b | ||
|
4b837343ff | ||
|
d1904ba156 | ||
|
3ed1bde38a | ||
|
5cd82df580 | ||
|
fdd0e82099 | ||
|
f540245aec | ||
|
7b3ca228ef | ||
|
aa23adfd8a | ||
|
95ee5d54e8 | ||
|
eac0e7a656 | ||
|
e41ec29867 | ||
|
42a922deb6 | ||
|
8e70d55d77 | ||
|
eeaf823ea9 | ||
|
2195e07998 | ||
|
70fb8fc8c6 | ||
|
b78ae1ef0d | ||
|
6af5566466 | ||
|
8ceca0a90d | ||
|
08fa6dbfc8 | ||
|
967aa758d3 | ||
|
e3b10fbdc2 | ||
|
2115e5c6ec | ||
|
41dc908032 | ||
|
2b2f24628d | ||
|
1cc5d1af33 | ||
|
88074cf5c3 | ||
|
5d637eed95 | ||
|
362c43ce5f | ||
|
622b8eb51e | ||
|
c369dcf781 | ||
|
53d1e1dee9 | ||
|
75f5825cff | ||
|
d05c90787c | ||
|
e07ba36a4a | ||
|
f7c05d9a08 | ||
|
59c5a1a35d | ||
|
3eb653821e | ||
|
214bc838c2 | ||
|
79c7ea5bab | ||
|
b59b1be6ff | ||
|
aeb75a6ce3 | ||
|
847325a119 | ||
|
26eabfaf6f | ||
|
40a7b97220 | ||
|
91b40748c4 | ||
|
013a2f94d6 | ||
|
74d280ed75 | ||
|
b7d46de10e | ||
|
c37b5bbbca | ||
|
5e08e73698 | ||
|
b27bb92d03 | ||
|
6bf8858cc6 | ||
|
c97a780645 | ||
|
976c1cd8e0 | ||
|
26fd6f8a6c | ||
|
0020ad28ba | ||
|
ba9ca63f10 | ||
|
28b5b732c2 | ||
|
b944da8445 | ||
|
5cab166d6b | ||
|
546cdb4229 | ||
|
77790fad1f | ||
|
bcf849c98f | ||
|
f7b3c8af02 | ||
|
cced60589c | ||
|
62adb32c94 | ||
|
6ab752dcdb | ||
|
4d6f071739 | ||
|
bd140c2ceb | ||
|
59d9991aa4 | ||
|
55e7f59e40 | ||
|
bb83483020 | ||
|
5300ef5119 | ||
|
5a50d4203c | ||
|
01a96f3086 | ||
|
0570d779ee | ||
|
18d0bc2c81 | ||
|
87e0d8148f | ||
|
53d8989145 | ||
|
5433b4ebdf | ||
|
f10cecf16a | ||
|
60a3f6f4cc | ||
|
f70ae89098 | ||
|
2f60c5e9b4 | ||
|
015ddd9517 | ||
|
f1539d9ec4 | ||
|
2d0444ba75 | ||
|
ed2c222e83 | ||
|
ed048913a4 | ||
|
121ba17698 | ||
|
d0f5a76c47 | ||
|
f809f1d089 | ||
|
070ad18781 | ||
|
56119ef1cc | ||
|
30195a35dc | ||
|
0500b7baab | ||
|
08473a5c25 | ||
|
37ad14c277 | ||
|
3e8c6ce541 | ||
|
3dd5539dc7 | ||
|
559a0320a8 | ||
|
8646d885f0 | ||
|
84c008cdce | ||
|
2671cb5b72 | ||
|
379440708f |
@@ -9,13 +9,12 @@
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"python.analysis.diagnosticMode": "workspace",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"ruff.organizeImports": false,
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "ms-python.black-formatter",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.ruff": true,
|
||||
"source.organizeImports": true
|
||||
"source.fixAll.ruff": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
},
|
||||
"[javascript]": {
|
||||
|
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
open_collective: nonebot
|
||||
custom: ["https://afdian.net/@nonebot"]
|
||||
custom: ["https://afdian.com/@nonebot"]
|
||||
|
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -35,3 +35,12 @@ updates:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: devcontainers
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
devcontainers:
|
||||
patterns:
|
||||
- "*"
|
||||
|
2
.github/workflows/release-drafter.yml
vendored
2
.github/workflows/release-drafter.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Update Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
changelog_file: website/src/changelog/changelog.md
|
||||
latest_changes_position: '# 更新日志\n\n'
|
||||
latest_changes_title: "## 最近更新"
|
||||
replace_regex: '(?<=## 最近更新\n)[\s\S]*?(?=\n## )'
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Archive Changelog
|
||||
uses: docker://ghcr.io/nonebot/auto-changelog:master
|
||||
with:
|
||||
changelog_file: website/src/pages/changelog.md
|
||||
changelog_file: website/src/changelog/changelog.md
|
||||
archive_regex: '(?<=## )最近更新(?=\n)'
|
||||
archive_title: ${{ env.TAG_NAME }}
|
||||
commit_and_push: false
|
||||
|
99
.github/workflows/website-preview-cd.yml
vendored
Normal file
99
.github/workflows/website-preview-cd.yml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: Site Deploy (Preview CD)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Site Deploy (Preview CI)"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
preview-cd:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
|
||||
environment: pull request
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
statuses: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Set Commit Status
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.payload.workflow_run.head_sha,
|
||||
context: 'Website Preview',
|
||||
description: 'Deploying...',
|
||||
state: 'pending',
|
||||
})
|
||||
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: website-preview
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Restore Context
|
||||
run: |
|
||||
cat action.env >> $GITHUB_ENV
|
||||
|
||||
- name: Set Deploy Name
|
||||
run: |
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ env.PR_NUMBER }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: deploy
|
||||
uses: nwtgck/actions-netlify@v3
|
||||
with:
|
||||
publish-dir: ./website/build
|
||||
production-deploy: false
|
||||
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.event.workflow_run.head_sha }}"
|
||||
alias: ${{ env.DEPLOY_NAME }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
||||
|
||||
# action netlify has no pull request context, so we need to comment by ourselves
|
||||
- name: Comment on Pull Request
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: website
|
||||
number: ${{ env.PR_NUMBER }}
|
||||
message: |
|
||||
:rocket: Deployed to ${{ steps.deploy.outputs.deploy-url }}
|
||||
|
||||
- name: Set Commit Status
|
||||
uses: actions/github-script@v7
|
||||
if: always()
|
||||
with:
|
||||
script: |
|
||||
if (`${{ job.status }}` === 'success') {
|
||||
github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.payload.workflow_run.head_sha,
|
||||
context: 'Website Preview',
|
||||
description: `Deployed to ${{ steps.deploy.outputs.deploy-url }}`,
|
||||
state: 'success',
|
||||
target_url: `${{ steps.deploy.outputs.deploy-url }}`,
|
||||
})
|
||||
} else {
|
||||
github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.payload.workflow_run.head_sha,
|
||||
context: 'Website Preview',
|
||||
description: `Deploy ${{ job.status }}`,
|
||||
state: 'failure',
|
||||
})
|
||||
}
|
42
.github/workflows/website-preview-ci.yml
vendored
Normal file
42
.github/workflows/website-preview-ci.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Site Deploy (Preview CI)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
preview-ci:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-preview-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build API Doc
|
||||
uses: ./.github/actions/build-api-doc
|
||||
|
||||
- name: Build Doc
|
||||
run: yarn build
|
||||
|
||||
- name: Export Context
|
||||
run: |
|
||||
echo "PR_NUMBER=${{ github.event.number }}" >> ./action.env
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: website-preview
|
||||
path: |
|
||||
./website/build
|
||||
./action.env
|
||||
retention-days: 1
|
46
.github/workflows/website-preview.yml
vendored
46
.github/workflows/website-preview.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Site Deploy(Preview)
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: pull-request-preview-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python Environment
|
||||
uses: ./.github/actions/setup-python
|
||||
|
||||
- name: Setup Node Environment
|
||||
uses: ./.github/actions/setup-node
|
||||
|
||||
- name: Build API Doc
|
||||
uses: ./.github/actions/build-api-doc
|
||||
|
||||
- name: Build Doc
|
||||
run: yarn build
|
||||
|
||||
- name: Get Deploy Name
|
||||
run: |
|
||||
echo "DEPLOY_NAME=deploy-preview-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Deploy to Netlify
|
||||
uses: nwtgck/actions-netlify@v3
|
||||
with:
|
||||
publish-dir: "./website/build"
|
||||
production-deploy: false
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
deploy-message: "Deploy ${{ env.DEPLOY_NAME }}@${{ github.sha }}"
|
||||
enable-commit-comment: false
|
||||
alias: ${{ env.DEPLOY_NAME }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.SITE_ID }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,6 +7,7 @@ docs_build/_build
|
||||
!tests/.env
|
||||
.docusaurus
|
||||
website/docs/api/**/*.md
|
||||
website/src/pages/changelog/**/*
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,node,visualstudiocode,jetbrains,macos,windows,linux
|
||||
|
@@ -7,30 +7,23 @@ ci:
|
||||
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.0
|
||||
rev: v0.7.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
stages: [commit]
|
||||
stages: [pre-commit]
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
stages: [commit]
|
||||
stages: [pre-commit]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.4.2
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
stages: [commit]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [javascript, jsx, ts, tsx, markdown, yaml, json]
|
||||
stages: [commit]
|
||||
stages: [pre-commit]
|
||||
|
||||
- repo: https://github.com/nonebot/nonemoji
|
||||
rev: v0.1.4
|
||||
|
@@ -1,3 +1,3 @@
|
||||
# Changelog
|
||||
|
||||
See [changelog.md](./website/src/pages/changelog.md) or <https://nonebot.dev/changelog>
|
||||
See [changelog.md](./website/src/changelog/changelog.md) or <https://nonebot.dev/changelog>
|
||||
|
@@ -126,7 +126,6 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
| Mirai([仓库](https://github.com/nonebot/adapter-mirai),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ✅ | QQ 协议 |
|
||||
| 钉钉([仓库](https://github.com/nonebot/adapter-ding),[协议](https://open.dingtalk.com/document/)) | 🤗 | 寻找 Maintainer(暂不可用) |
|
||||
| 开黑啦([仓库](https://github.com/Tian-que/nonebot-adapter-kaiheila),[协议](https://developer.kookapp.cn/)) | ↗️ | 由社区贡献 |
|
||||
| Mirai([仓库](https://github.com/ieew/nonebot_adapter_mirai2),[协议](https://docs.mirai.mamoe.net/mirai-api-http/)) | ↗️ | QQ 协议,由社区贡献 |
|
||||
| Ntchat([仓库](https://github.com/JustUndertaker/adapter-ntchat)) | ↗️ | 微信协议,由社区贡献 |
|
||||
| MineCraft([仓库](https://github.com/17TheWord/nonebot-adapter-minecraft)) | ↗️ | 由社区贡献 |
|
||||
| BiliBili Live([仓库](https://github.com/wwweww/adapter-bilibili)) | ↗️ | 由社区贡献 |
|
||||
|
@@ -69,16 +69,6 @@
|
||||
"tags": [],
|
||||
"is_official": true
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.mirai2",
|
||||
"project_link": "nonebot_adapter_mirai2",
|
||||
"name": "mirai2",
|
||||
"desc": "为 nonebot2 添加 mirai_api_http2.x的兼容适配器",
|
||||
"author": "ieew",
|
||||
"homepage": "https://github.com/ieew/nonebot_adapter_mirai2",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"module_name": "nonebot.adapters.onebot.v12",
|
||||
"project_link": "nonebot-adapter-onebot",
|
||||
|
@@ -607,5 +607,55 @@
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "星辰 Bot",
|
||||
"desc": "欢迎使用 星辰 Bot 项目!这是一款基于 NoneBot2 打造的智能 QQ 机器人,旨在为用户提供丰富的功能体验。无论是获取一言的灵感,探索历史上的今天,还是穿梭60s世界,星辰 Bot 为您打开了全新的交流之门。快来尝试吧!",
|
||||
"author": "StarXinXin",
|
||||
"homepage": "https://github.com/StarXinXin/StarsBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "Minecraft_QQBot",
|
||||
"desc": "基于 NoneBot2 的 Minecraft 群服互联 QQ 机器人,支持多服务器多种方式连接。",
|
||||
"author": "Lonely-Sails",
|
||||
"homepage": "https://github.com/Minecraft-QQBot/BotServer",
|
||||
"tags": [
|
||||
{
|
||||
"label": "Minecraft",
|
||||
"color": "#ea5252"
|
||||
},
|
||||
{
|
||||
"label": "娱乐",
|
||||
"color": "#37a7e7"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "小安提Bot",
|
||||
"desc": "服务于音游 舞萌DX 的多功能Bot",
|
||||
"author": "Ant1816",
|
||||
"homepage": "https://github.com/Ant1816/Ant1Bot",
|
||||
"tags": [
|
||||
{
|
||||
"label": "maimaiDX",
|
||||
"color": "#52ea9a"
|
||||
},
|
||||
{
|
||||
"label": "音游",
|
||||
"color": "#f74b18"
|
||||
}
|
||||
],
|
||||
"is_official": false
|
||||
},
|
||||
{
|
||||
"name": "CanrotBot",
|
||||
"desc": "有很多实用功能的bot,也有很多没什么用的娱乐功能;接入了大模型,并且有一部分功能可以被大模型调用。主打一个全都有(",
|
||||
"author": "wangyw15",
|
||||
"homepage": "https://github.com/wangyw15/CanrotBot",
|
||||
"tags": [],
|
||||
"is_official": false
|
||||
}
|
||||
]
|
||||
|
1291
assets/plugins.json
1291
assets/plugins.json
File diff suppressed because it is too large
Load Diff
2634
envs/pydantic-v1/poetry.lock
generated
2634
envs/pydantic-v1/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
2737
envs/pydantic-v2/poetry.lock
generated
2737
envs/pydantic-v2/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
1384
envs/test/poetry.lock
generated
1384
envs/test/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,11 @@ packages = [{ include = "nonebot-test.py" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
nonebug = "^0.3.7"
|
||||
trio = "^0.27.0"
|
||||
nonebug = "^0.4.1"
|
||||
wsproto = "^1.2.0"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-xdist = "^3.0.2"
|
||||
pytest-asyncio = "^0.23.2"
|
||||
werkzeug = ">=2.3.6,<4.0.0"
|
||||
coverage-conditional-plugin = "^0.9.0"
|
||||
|
||||
|
@@ -39,6 +39,8 @@
|
||||
- `require` => {ref}``require` <nonebot.plugin.load.require>`
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot 模块
|
||||
"""
|
||||
|
@@ -3,6 +3,8 @@
|
||||
使用 {ref}`nonebot.drivers.Driver.register_adapter` 注册适配器。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.adapters 模块
|
||||
"""
|
||||
|
@@ -3,22 +3,27 @@
|
||||
为兼容 Pydantic V1 与 V2 版本,定义了一系列兼容函数与类供使用。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 16
|
||||
description: nonebot.compat 模块
|
||||
"""
|
||||
|
||||
from collections.abc import Generator
|
||||
from functools import cached_property
|
||||
from dataclasses import dataclass, is_dataclass
|
||||
from typing_extensions import Self, get_args, get_origin, is_typeddict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
Optional,
|
||||
Protocol,
|
||||
Annotated,
|
||||
overload,
|
||||
)
|
||||
|
||||
from pydantic import VERSION, BaseModel
|
||||
@@ -46,8 +51,8 @@ __all__ = (
|
||||
"DEFAULT_CONFIG",
|
||||
"FieldInfo",
|
||||
"ModelField",
|
||||
"TypeAdapter",
|
||||
"extract_field_info",
|
||||
"model_field_validate",
|
||||
"model_fields",
|
||||
"model_config",
|
||||
"model_dump",
|
||||
@@ -63,9 +68,10 @@ __autodoc__ = {
|
||||
|
||||
|
||||
if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic import TypeAdapter as TypeAdapter
|
||||
from pydantic_core import CoreSchema, core_schema
|
||||
from pydantic._internal._repr import display_as_type
|
||||
from pydantic import TypeAdapter, GetCoreSchemaHandler
|
||||
from pydantic.fields import FieldInfo as BaseFieldInfo
|
||||
|
||||
Required = Ellipsis
|
||||
@@ -125,6 +131,25 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
"""Construct a ModelField from given infos."""
|
||||
return cls._construct(name, annotation, field_info or FieldInfo())
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes,
|
||||
# to allow store them in a set.
|
||||
return id(self)
|
||||
|
||||
@cached_property
|
||||
def type_adapter(self) -> TypeAdapter:
|
||||
"""TypeAdapter of the field.
|
||||
|
||||
Cache the TypeAdapter to avoid creating it multiple times.
|
||||
Pydantic v2 uses too much cpu time to create TypeAdapter.
|
||||
|
||||
See: https://github.com/pydantic/pydantic/issues/9834
|
||||
"""
|
||||
return TypeAdapter(
|
||||
Annotated[self.annotation, self.field_info],
|
||||
config=None if self._annotation_has_config() else DEFAULT_CONFIG,
|
||||
)
|
||||
|
||||
def _annotation_has_config(self) -> bool:
|
||||
"""Check if the annotation has config.
|
||||
|
||||
@@ -152,10 +177,9 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
"""Get the display of the type of the field."""
|
||||
return display_as_type(self.annotation)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Each ModelField is unique for our purposes,
|
||||
# to allow store them in a set.
|
||||
return id(self)
|
||||
def validate_value(self, value: Any) -> Any:
|
||||
"""Validate the value pass to the field."""
|
||||
return self.type_adapter.validate_python(value)
|
||||
|
||||
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
|
||||
"""Get FieldInfo init kwargs from a FieldInfo instance."""
|
||||
@@ -164,15 +188,6 @@ if PYDANTIC_V2: # pragma: pydantic-v2
|
||||
kwargs["annotation"] = field_info.rebuild_annotation()
|
||||
return kwargs
|
||||
|
||||
def model_field_validate(
|
||||
model_field: ModelField, value: Any, config: Optional[ConfigDict] = None
|
||||
) -> Any:
|
||||
"""Validate the value pass to the field."""
|
||||
type: Any = Annotated[model_field.annotation, model_field.field_info]
|
||||
return TypeAdapter(
|
||||
type, config=None if model_field._annotation_has_config() else config
|
||||
).validate_python(value)
|
||||
|
||||
def model_fields(model: type[BaseModel]) -> list[ModelField]:
|
||||
"""Get field list of a model."""
|
||||
|
||||
@@ -305,6 +320,45 @@ else: # pragma: pydantic-v1
|
||||
)
|
||||
return cls._construct(name, annotation, field_info or FieldInfo())
|
||||
|
||||
def validate_value(self, value: Any) -> Any:
|
||||
"""Validate the value pass to the field."""
|
||||
v, errs_ = self.validate(value, {}, loc=())
|
||||
if errs_:
|
||||
raise ValueError(value, self)
|
||||
return v
|
||||
|
||||
class TypeAdapter(Generic[T]):
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
type: type[T],
|
||||
*,
|
||||
config: Optional[ConfigDict] = ...,
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
type: Any,
|
||||
*,
|
||||
config: Optional[ConfigDict] = ...,
|
||||
) -> None: ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: Any,
|
||||
*,
|
||||
config: Optional[ConfigDict] = None,
|
||||
) -> None:
|
||||
self.type = type
|
||||
self.config = config
|
||||
|
||||
def validate_python(self, value: Any) -> T:
|
||||
return type_validate_python(self.type, value)
|
||||
|
||||
def validate_json(self, value: Union[str, bytes]) -> T:
|
||||
return type_validate_json(self.type, value)
|
||||
|
||||
def extract_field_info(field_info: BaseFieldInfo) -> dict[str, Any]:
|
||||
"""Get FieldInfo init kwargs from a FieldInfo instance."""
|
||||
|
||||
@@ -314,22 +368,6 @@ else: # pragma: pydantic-v1
|
||||
kwargs.update(field_info.extra)
|
||||
return kwargs
|
||||
|
||||
def model_field_validate(
|
||||
model_field: ModelField, value: Any, config: Optional[type[ConfigDict]] = None
|
||||
) -> Any:
|
||||
"""Validate the value pass to the field.
|
||||
|
||||
Set config before validate to ensure validate correctly.
|
||||
"""
|
||||
|
||||
if model_field.model_config is not config:
|
||||
model_field.set_config(config or ConfigDict)
|
||||
|
||||
v, errs_ = model_field.validate(value, {}, loc=())
|
||||
if errs_:
|
||||
raise ValueError(value, model_field)
|
||||
return v
|
||||
|
||||
def model_fields(model: type[BaseModel]) -> list[ModelField]:
|
||||
"""Get field list of a model."""
|
||||
|
||||
|
@@ -7,6 +7,8 @@ NoneBot 使用 [`pydantic`](https://pydantic-docs.helpmanual.io/) 以及
|
||||
详情见 [`pydantic Field Type`](https://pydantic-docs.helpmanual.io/usage/types/) 文档。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.config 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块包含了 NoneBot 事件处理过程中使用到的常量。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 9
|
||||
description: nonebot.consts 模块
|
||||
"""
|
||||
|
@@ -1,22 +1,32 @@
|
||||
"""本模块模块实现了依赖注入的定义与处理。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.dependencies 模块
|
||||
"""
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import inspect
|
||||
from functools import partial
|
||||
from dataclasses import field, dataclass
|
||||
from collections.abc import Iterable, Awaitable
|
||||
from typing import Any, Generic, TypeVar, Callable, Optional, cast
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import _DependentCallable
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined
|
||||
from nonebot.utils import (
|
||||
run_sync,
|
||||
run_coro_with_shield,
|
||||
is_coroutine_callable,
|
||||
flatten_exception_group,
|
||||
)
|
||||
|
||||
from .utils import check_field_type, get_typed_signature
|
||||
|
||||
@@ -82,7 +92,16 @@ class Dependent(Generic[R]):
|
||||
)
|
||||
|
||||
async def __call__(self, **kwargs: Any) -> R:
|
||||
try:
|
||||
exception: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||
|
||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||
nonlocal exception
|
||||
exception = exc_group
|
||||
# raise one of the exceptions instead
|
||||
excs = list(flatten_exception_group(exc_group))
|
||||
logger.trace(f"{self} skipped due to {excs}")
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
# do pre-check
|
||||
await self.check(**kwargs)
|
||||
|
||||
@@ -94,9 +113,8 @@ class Dependent(Generic[R]):
|
||||
return await cast(Callable[..., Awaitable[R]], self.call)(**values)
|
||||
else:
|
||||
return await run_sync(cast(Callable[..., R], self.call))(**values)
|
||||
except SkippedException as e:
|
||||
logger.trace(f"{self} skipped due to {e}")
|
||||
raise
|
||||
|
||||
raise exception
|
||||
|
||||
@staticmethod
|
||||
def parse_params(
|
||||
@@ -164,9 +182,16 @@ class Dependent(Generic[R]):
|
||||
return cls(call, params, parameterless_params)
|
||||
|
||||
async def check(self, **params: Any) -> None:
|
||||
await asyncio.gather(*(param._check(**params) for param in self.parameterless))
|
||||
await asyncio.gather(
|
||||
*(cast(Param, param.field_info)._check(**params) for param in self.params)
|
||||
if self.parameterless:
|
||||
async with anyio.create_task_group() as tg:
|
||||
for param in self.parameterless:
|
||||
tg.start_soon(partial(param._check, **params))
|
||||
|
||||
if self.params:
|
||||
async with anyio.create_task_group() as tg:
|
||||
for param in self.params:
|
||||
tg.start_soon(
|
||||
partial(cast(Param, param.field_info)._check, **params)
|
||||
)
|
||||
|
||||
async def _solve_field(self, field: ModelField, params: dict[str, Any]) -> Any:
|
||||
@@ -183,10 +208,22 @@ class Dependent(Generic[R]):
|
||||
await param._solve(**params)
|
||||
|
||||
# solve param values
|
||||
values = await asyncio.gather(
|
||||
*(self._solve_field(field, params) for field in self.params)
|
||||
)
|
||||
return {field.name: value for field, value in zip(self.params, values)}
|
||||
result: dict[str, Any] = {}
|
||||
if not self.params:
|
||||
return result
|
||||
|
||||
async def _solve_field(field: ModelField, params: dict[str, Any]) -> None:
|
||||
value = await self._solve_field(field, params)
|
||||
result[field.name] = value
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
for field in self.params:
|
||||
# shield the task to prevent cancellation
|
||||
# when one of the tasks raises an exception
|
||||
# this will improve the dependency cache reusability
|
||||
tg.start_soon(run_coro_with_shield, _solve_field(field, params))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
__autodoc__ = {"CustomConfig": False}
|
||||
|
@@ -1,5 +1,7 @@
|
||||
"""
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.dependencies.utils 模块
|
||||
"""
|
||||
@@ -9,9 +11,9 @@ from typing import Any, Callable, ForwardRef
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from nonebot.compat import ModelField
|
||||
from nonebot.exception import TypeMisMatch
|
||||
from nonebot.typing import evaluate_forwardref
|
||||
from nonebot.compat import DEFAULT_CONFIG, ModelField, model_field_validate
|
||||
|
||||
|
||||
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
||||
@@ -51,6 +53,6 @@ def check_field_type(field: ModelField, value: Any) -> Any:
|
||||
"""检查字段类型是否匹配"""
|
||||
|
||||
try:
|
||||
return model_field_validate(field, value, DEFAULT_CONFIG)
|
||||
return field.validate_value(value)
|
||||
except ValueError:
|
||||
raise TypeMisMatch(field, value)
|
||||
|
@@ -3,6 +3,8 @@
|
||||
各驱动请继承以下基类。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.drivers 模块
|
||||
"""
|
||||
|
@@ -11,6 +11,8 @@ pip install nonebot2[aiohttp]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.drivers.aiohttp 模块
|
||||
"""
|
||||
|
@@ -11,6 +11,8 @@ pip install nonebot2[fastapi]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.drivers.fastapi 模块
|
||||
"""
|
||||
|
@@ -11,6 +11,8 @@ pip install nonebot2[httpx]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.drivers.httpx 模块
|
||||
"""
|
||||
|
@@ -5,19 +5,25 @@
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 6
|
||||
description: nonebot.drivers.none 模块
|
||||
"""
|
||||
|
||||
import signal
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Optional
|
||||
from typing_extensions import override
|
||||
|
||||
import anyio
|
||||
from anyio.abc import TaskGroup
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.consts import WINDOWS
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.drivers import Driver as BaseDriver
|
||||
from nonebot.utils import flatten_exception_group
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
@@ -33,8 +39,8 @@ class Driver(BaseDriver):
|
||||
def __init__(self, env: Env, config: Config):
|
||||
super().__init__(env, config)
|
||||
|
||||
self.should_exit: asyncio.Event = asyncio.Event()
|
||||
self.force_exit: bool = False
|
||||
self.should_exit: anyio.Event = anyio.Event()
|
||||
self.force_exit: anyio.Event = anyio.Event()
|
||||
|
||||
@property
|
||||
@override
|
||||
@@ -52,85 +58,98 @@ class Driver(BaseDriver):
|
||||
def run(self, *args, **kwargs):
|
||||
"""启动 none driver"""
|
||||
super().run(*args, **kwargs)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self._serve())
|
||||
anyio.run(self._serve)
|
||||
|
||||
async def _serve(self):
|
||||
self._install_signal_handlers()
|
||||
await self._startup()
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
await self._main_loop()
|
||||
await self._shutdown()
|
||||
async with anyio.create_task_group() as driver_tg:
|
||||
driver_tg.start_soon(self._handle_signals)
|
||||
driver_tg.start_soon(self._listen_force_exit, driver_tg)
|
||||
driver_tg.start_soon(self._handle_lifespan, driver_tg)
|
||||
|
||||
async def _startup(self):
|
||||
async def _handle_signals(self):
|
||||
try:
|
||||
await self._lifespan.startup()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Application startup failed. "
|
||||
"Exiting.</bg #f8bbd0></r>"
|
||||
)
|
||||
self.should_exit.set()
|
||||
return
|
||||
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def _main_loop(self):
|
||||
await self.should_exit.wait()
|
||||
|
||||
async def _shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
|
||||
logger.info("Waiting for application shutdown.")
|
||||
|
||||
try:
|
||||
await self._lifespan.shutdown()
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running shutdown function. "
|
||||
"Ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
for task in asyncio.all_tasks():
|
||||
if task is not asyncio.current_task() and not task.done():
|
||||
task.cancel()
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
if tasks and not self.force_exit:
|
||||
logger.info("Waiting for tasks to finish. (CTRL+C to force quit)")
|
||||
while tasks and not self.force_exit:
|
||||
await asyncio.sleep(0.1)
|
||||
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
logger.info("Application shutdown complete.")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.stop()
|
||||
|
||||
def _install_signal_handlers(self) -> None:
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
# Signals can only be listened to from the main thread.
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
for sig in HANDLED_SIGNALS:
|
||||
loop.add_signal_handler(sig, self._handle_exit, sig, None)
|
||||
with anyio.open_signal_receiver(*HANDLED_SIGNALS) as signal_receiver:
|
||||
async for sig in signal_receiver:
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
except NotImplementedError:
|
||||
# Windows
|
||||
for sig in HANDLED_SIGNALS:
|
||||
signal.signal(sig, self._handle_exit)
|
||||
signal.signal(sig, self._handle_legacy_signal)
|
||||
|
||||
def _handle_exit(self, sig, frame):
|
||||
# backport for Windows signal handling
|
||||
def _handle_legacy_signal(self, sig, frame):
|
||||
self.exit(force=self.should_exit.is_set())
|
||||
|
||||
async def _handle_lifespan(self, tg: TaskGroup):
|
||||
try:
|
||||
await self._startup()
|
||||
|
||||
if self.should_exit.is_set():
|
||||
return
|
||||
|
||||
await self._listen_exit()
|
||||
|
||||
await self._shutdown()
|
||||
finally:
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
async def _startup(self):
|
||||
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
self.should_exit.set()
|
||||
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error occurred while running startup hook."
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
logger.error(
|
||||
"<r><bg #f8bbd0>Application startup failed. "
|
||||
"Exiting.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch({Exception: handle_exception}):
|
||||
await self._lifespan.startup()
|
||||
|
||||
if not self.should_exit.is_set():
|
||||
logger.info("Application startup completed.")
|
||||
|
||||
async def _listen_exit(self, tg: Optional[TaskGroup] = None):
|
||||
await self.should_exit.wait()
|
||||
|
||||
if tg is not None:
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
async def _shutdown(self):
|
||||
logger.info("Shutting down")
|
||||
logger.info("Waiting for application shutdown. (CTRL+C to force quit)")
|
||||
|
||||
error_occurred: bool = False
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
nonlocal error_occurred
|
||||
|
||||
error_occurred = True
|
||||
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error occurred while running shutdown hook."
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
logger.error(
|
||||
"<r><bg #f8bbd0>Application shutdown failed. "
|
||||
"Exiting.</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch({Exception: handle_exception}):
|
||||
await self._lifespan.shutdown()
|
||||
|
||||
if not error_occurred:
|
||||
logger.info("Application shutdown complete.")
|
||||
|
||||
async def _listen_force_exit(self, tg: TaskGroup):
|
||||
await self.force_exit.wait()
|
||||
tg.cancel_scope.cancel()
|
||||
|
||||
def exit(self, force: bool = False):
|
||||
"""退出 none driver
|
||||
|
||||
@@ -140,4 +159,4 @@ class Driver(BaseDriver):
|
||||
if not self.should_exit.is_set():
|
||||
self.should_exit.set()
|
||||
if force:
|
||||
self.force_exit = True
|
||||
self.force_exit.set()
|
||||
|
@@ -11,6 +11,8 @@ pip install nonebot2[quart]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.drivers.quart 模块
|
||||
"""
|
||||
|
@@ -11,6 +11,8 @@ pip install nonebot2[websockets]
|
||||
:::
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 4
|
||||
description: nonebot.drivers.websockets 模块
|
||||
"""
|
||||
@@ -69,6 +71,8 @@ class Mixin(WebSocketClientMixin):
|
||||
@override
|
||||
@asynccontextmanager
|
||||
async def websocket(self, setup: Request) -> AsyncGenerator["WebSocket", None]:
|
||||
if setup.proxy is not None:
|
||||
logger.warning("proxy is not supported by websockets driver")
|
||||
connection = Connect(
|
||||
str(setup.url),
|
||||
extra_headers={**setup.headers, **setup.cookies.as_header(setup)},
|
||||
|
@@ -25,6 +25,8 @@ NoneBotException
|
||||
```
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 10
|
||||
description: nonebot.exception 模块
|
||||
"""
|
||||
|
@@ -1,11 +1,14 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional, Protocol
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Config
|
||||
from nonebot.exception import MockApiException
|
||||
from nonebot.utils import flatten_exception_group
|
||||
from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -76,48 +79,99 @@ class Bot(abc.ABC):
|
||||
skip_calling_api: bool = False
|
||||
exception: Optional[Exception] = None
|
||||
|
||||
if coros := [hook(self, api, data) for hook in self._calling_api_hook]:
|
||||
try:
|
||||
if self._calling_api_hook:
|
||||
logger.debug("Running CallingAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
except MockApiException as e:
|
||||
skip_calling_api = True
|
||||
result = e.result
|
||||
logger.debug(
|
||||
f"Calling API {api} is cancelled. Return {result} instead."
|
||||
|
||||
def _handle_mock_api_exception(
|
||||
exc_group: BaseExceptionGroup[MockApiException],
|
||||
) -> None:
|
||||
nonlocal skip_calling_api, result
|
||||
|
||||
excs = [
|
||||
exc
|
||||
for exc in flatten_exception_group(exc_group)
|
||||
if isinstance(exc, MockApiException)
|
||||
]
|
||||
if not excs:
|
||||
return
|
||||
elif len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple hooks want to mock API result. Use the first one."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
|
||||
skip_calling_api = True
|
||||
result = excs[0].result
|
||||
|
||||
logger.debug(
|
||||
f"Calling API {api} is cancelled. Return {result!r} instead."
|
||||
)
|
||||
|
||||
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error when running CallingAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch(
|
||||
{
|
||||
MockApiException: _handle_mock_api_exception,
|
||||
Exception: _handle_exception,
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for hook in self._calling_api_hook:
|
||||
tg.start_soon(hook, self, api, data)
|
||||
|
||||
if not skip_calling_api:
|
||||
try:
|
||||
result = await self.adapter._call_api(self, api, **data)
|
||||
except Exception as e:
|
||||
exception = e
|
||||
|
||||
if coros := [
|
||||
hook(self, exception, api, data, result) for hook in self._called_api_hook
|
||||
]:
|
||||
try:
|
||||
if self._called_api_hook:
|
||||
logger.debug("Running CalledAPI hooks...")
|
||||
await asyncio.gather(*coros)
|
||||
except MockApiException as e:
|
||||
# mock api result
|
||||
result = e.result
|
||||
# ignore exception
|
||||
|
||||
def _handle_mock_api_exception(
|
||||
exc_group: BaseExceptionGroup[MockApiException],
|
||||
) -> None:
|
||||
nonlocal result, exception
|
||||
|
||||
excs = [
|
||||
exc
|
||||
for exc in flatten_exception_group(exc_group)
|
||||
if isinstance(exc, MockApiException)
|
||||
]
|
||||
if not excs:
|
||||
return
|
||||
elif len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple hooks want to mock API result. Use the first one."
|
||||
)
|
||||
|
||||
result = excs[0].result
|
||||
exception = None
|
||||
logger.debug(
|
||||
f"Calling API {api} result is mocked. Return {result} instead."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
|
||||
def _handle_exception(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>Error when running CalledAPI hook. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
with catch(
|
||||
{
|
||||
MockApiException: _handle_mock_api_exception,
|
||||
Exception: _handle_exception,
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for hook in self._called_api_hook:
|
||||
tg.start_soon(hook, self, exception, api, data, result)
|
||||
|
||||
if exception:
|
||||
raise exception
|
||||
return result
|
||||
|
@@ -1,6 +1,11 @@
|
||||
from collections.abc import Awaitable
|
||||
from types import TracebackType
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import Any, Union, Callable, cast
|
||||
from collections.abc import Iterable, Awaitable
|
||||
from typing import Any, Union, Callable, Optional, cast
|
||||
|
||||
import anyio
|
||||
from anyio.abc import TaskGroup
|
||||
from exceptiongroup import suppress
|
||||
|
||||
from nonebot.utils import run_sync, is_coroutine_callable
|
||||
|
||||
@@ -11,10 +16,24 @@ LIFESPAN_FUNC: TypeAlias = Union[SYNC_LIFESPAN_FUNC, ASYNC_LIFESPAN_FUNC]
|
||||
|
||||
class Lifespan:
|
||||
def __init__(self) -> None:
|
||||
self._task_group: Optional[TaskGroup] = None
|
||||
|
||||
self._startup_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._ready_funcs: list[LIFESPAN_FUNC] = []
|
||||
self._shutdown_funcs: list[LIFESPAN_FUNC] = []
|
||||
|
||||
@property
|
||||
def task_group(self) -> TaskGroup:
|
||||
if self._task_group is None:
|
||||
raise RuntimeError("Lifespan not started")
|
||||
return self._task_group
|
||||
|
||||
@task_group.setter
|
||||
def task_group(self, task_group: TaskGroup) -> None:
|
||||
if self._task_group is not None:
|
||||
raise RuntimeError("Lifespan already started")
|
||||
self._task_group = task_group
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
self._startup_funcs.append(func)
|
||||
return func
|
||||
@@ -29,7 +48,7 @@ class Lifespan:
|
||||
|
||||
@staticmethod
|
||||
async def _run_lifespan_func(
|
||||
funcs: list[LIFESPAN_FUNC],
|
||||
funcs: Iterable[LIFESPAN_FUNC],
|
||||
) -> None:
|
||||
for func in funcs:
|
||||
if is_coroutine_callable(func):
|
||||
@@ -38,18 +57,44 @@ class Lifespan:
|
||||
await run_sync(cast(SYNC_LIFESPAN_FUNC, func))()
|
||||
|
||||
async def startup(self) -> None:
|
||||
# create background task group
|
||||
self.task_group = anyio.create_task_group()
|
||||
await self.task_group.__aenter__()
|
||||
|
||||
# run startup funcs
|
||||
if self._startup_funcs:
|
||||
await self._run_lifespan_func(self._startup_funcs)
|
||||
|
||||
# run ready funcs
|
||||
if self._ready_funcs:
|
||||
await self._run_lifespan_func(self._ready_funcs)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
async def shutdown(
|
||||
self,
|
||||
*,
|
||||
exc_type: Optional[type[BaseException]] = None,
|
||||
exc_val: Optional[BaseException] = None,
|
||||
exc_tb: Optional[TracebackType] = None,
|
||||
) -> None:
|
||||
if self._shutdown_funcs:
|
||||
await self._run_lifespan_func(self._shutdown_funcs)
|
||||
# reverse shutdown funcs to ensure stack order
|
||||
await self._run_lifespan_func(reversed(self._shutdown_funcs))
|
||||
|
||||
# shutdown background task group
|
||||
self.task_group.cancel_scope.cancel()
|
||||
|
||||
with suppress(Exception):
|
||||
await self.task_group.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
self._task_group = None
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
await self.startup()
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
await self.shutdown()
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
await self.shutdown(exc_type=exc_type, exc_val=exc_val, exc_tb=exc_tb)
|
||||
|
@@ -1,17 +1,20 @@
|
||||
import abc
|
||||
import asyncio
|
||||
from types import TracebackType
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing_extensions import Self, TypeAlias
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, Union, ClassVar, Optional
|
||||
|
||||
from anyio.abc import TaskGroup
|
||||
from anyio import CancelScope, create_task_group
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.config import Env, Config
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.internal.params import BotParam, DependParam, DefaultParam
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch, flatten_exception_group
|
||||
from nonebot.typing import (
|
||||
T_DependencyCache,
|
||||
T_BotConnectionHook,
|
||||
@@ -61,7 +64,6 @@ class Driver(abc.ABC):
|
||||
self.config: Config = config
|
||||
"""全局配置对象"""
|
||||
self._bots: dict[str, "Bot"] = {}
|
||||
self._bot_tasks: set[asyncio.Task] = set()
|
||||
self._lifespan = Lifespan()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -75,6 +77,10 @@ class Driver(abc.ABC):
|
||||
"""获取当前所有已连接的 Bot"""
|
||||
return self._bots
|
||||
|
||||
@property
|
||||
def task_group(self) -> TaskGroup:
|
||||
return self._lifespan.task_group
|
||||
|
||||
def register_adapter(self, adapter: type["Adapter"], **kwargs) -> None:
|
||||
"""注册一个协议适配器
|
||||
|
||||
@@ -112,8 +118,6 @@ class Driver(abc.ABC):
|
||||
f"<g>Loaded adapters: {escape_tag(', '.join(self._adapters))}</g>"
|
||||
)
|
||||
|
||||
self.on_shutdown(self._cleanup)
|
||||
|
||||
def on_startup(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
|
||||
"""注册一个启动时执行的函数"""
|
||||
return self._lifespan.on_startup(func)
|
||||
@@ -154,66 +158,63 @@ class Driver(abc.ABC):
|
||||
raise RuntimeError(f"Duplicate bot connection with id {bot.self_id}")
|
||||
self._bots[bot.self_id] = bot
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_connection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
if not self._bot_connection_hook:
|
||||
return
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketConnection hook. "
|
||||
"Running cancelled!"
|
||||
"Error when running WebSocketConnection hook:"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
task = asyncio.create_task(_run_hook(bot))
|
||||
task.add_done_callback(self._bot_tasks.discard)
|
||||
self._bot_tasks.add(task)
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||
async with AsyncExitStack() as stack, create_task_group() as tg:
|
||||
for hook in self._bot_connection_hook:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
hook(
|
||||
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
|
||||
self.task_group.start_soon(_run_hook, bot)
|
||||
|
||||
def _bot_disconnect(self, bot: "Bot") -> None:
|
||||
"""在连接断开后,调用该函数来注销 bot 对象"""
|
||||
if bot.self_id in self._bots:
|
||||
del self._bots[bot.self_id]
|
||||
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
async with AsyncExitStack() as stack:
|
||||
if coros := [
|
||||
run_coro_with_catch(
|
||||
hook(bot=bot, stack=stack, dependency_cache=dependency_cache),
|
||||
(SkippedException,),
|
||||
)
|
||||
for hook in self._bot_disconnection_hook
|
||||
]:
|
||||
try:
|
||||
await asyncio.gather(*coros)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
if not self._bot_disconnection_hook:
|
||||
return
|
||||
|
||||
def handle_exception(exc_group: BaseExceptionGroup) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(
|
||||
"<r><bg #f8bbd0>"
|
||||
"Error when running WebSocketDisConnection hook. "
|
||||
"Running cancelled!"
|
||||
"Error when running WebSocketDisConnection hook:"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
task = asyncio.create_task(_run_hook(bot))
|
||||
task.add_done_callback(self._bot_tasks.discard)
|
||||
self._bot_tasks.add(task)
|
||||
|
||||
async def _cleanup(self) -> None:
|
||||
"""清理驱动器资源"""
|
||||
if self._bot_tasks:
|
||||
logger.opt(colors=True).debug(
|
||||
"<y>Waiting for running bot connection hooks...</y>"
|
||||
async def _run_hook(bot: "Bot") -> None:
|
||||
dependency_cache: T_DependencyCache = {}
|
||||
# shield cancellation to ensure bot disconnect hooks are always run
|
||||
with CancelScope(shield=True), catch({Exception: handle_exception}):
|
||||
async with create_task_group() as tg, AsyncExitStack() as stack:
|
||||
for hook in self._bot_disconnection_hook:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
hook(
|
||||
bot=bot, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
await asyncio.gather(*self._bot_tasks, return_exceptions=True)
|
||||
|
||||
self.task_group.start_soon(_run_hook, bot)
|
||||
|
||||
|
||||
class Mixin(abc.ABC):
|
||||
|
@@ -15,7 +15,7 @@ def combine_driver(driver: type[D]) -> type[D]: ...
|
||||
|
||||
@overload
|
||||
def combine_driver(
|
||||
driver: type[D], _m: type[Mixin], *mixins: type[Mixin]
|
||||
driver: type[D], __m: type[Mixin], /, *mixins: type[Mixin]
|
||||
) -> type["CombinedDriver"]: ...
|
||||
|
||||
|
||||
|
@@ -22,11 +22,13 @@ from typing import ( # noqa: UP035
|
||||
overload,
|
||||
)
|
||||
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.internal.rule import Rule
|
||||
from nonebot.utils import classproperty
|
||||
from nonebot.dependencies import Param, Dependent
|
||||
from nonebot.internal.permission import User, Permission
|
||||
from nonebot.utils import classproperty, flatten_exception_group
|
||||
from nonebot.internal.adapter import (
|
||||
Bot,
|
||||
Event,
|
||||
@@ -76,7 +78,7 @@ T = TypeVar("T")
|
||||
current_bot: ContextVar[Bot] = ContextVar("current_bot")
|
||||
current_event: ContextVar[Event] = ContextVar("current_event")
|
||||
current_matcher: ContextVar["Matcher"] = ContextVar("current_matcher")
|
||||
current_handler: ContextVar[Dependent] = ContextVar("current_handler")
|
||||
current_handler: ContextVar[Dependent[Any]] = ContextVar("current_handler")
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -812,8 +814,12 @@ class Matcher(metaclass=MatcherMeta):
|
||||
f"bot={bot}, event={event!r}, state={state!r}"
|
||||
)
|
||||
|
||||
def _handle_stop_propagation(exc_group: BaseExceptionGroup[StopPropagation]):
|
||||
self.block = True
|
||||
|
||||
with self.ensure_context(bot, event):
|
||||
try:
|
||||
with catch({StopPropagation: _handle_stop_propagation}):
|
||||
# Refresh preprocess state
|
||||
self.state.update(state)
|
||||
|
||||
@@ -821,7 +827,13 @@ class Matcher(metaclass=MatcherMeta):
|
||||
handler = self.remain_handlers.pop(0)
|
||||
current_handler.set(handler)
|
||||
logger.debug(f"Running handler {handler}")
|
||||
try:
|
||||
|
||||
def _handle_skipped(
|
||||
exc_group: BaseExceptionGroup[SkippedException],
|
||||
):
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
await handler(
|
||||
matcher=self,
|
||||
bot=bot,
|
||||
@@ -830,10 +842,6 @@ class Matcher(metaclass=MatcherMeta):
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
except SkippedException:
|
||||
logger.debug(f"Handler {handler} skipped")
|
||||
except StopPropagation:
|
||||
self.block = True
|
||||
finally:
|
||||
logger.info(f"{self} running complete")
|
||||
|
||||
@@ -846,10 +854,54 @@ class Matcher(metaclass=MatcherMeta):
|
||||
stack: Optional[AsyncExitStack] = None,
|
||||
dependency_cache: Optional[T_DependencyCache] = None,
|
||||
):
|
||||
try:
|
||||
exc: Optional[Union[FinishedException, RejectedException, PausedException]] = (
|
||||
None
|
||||
)
|
||||
|
||||
def _handle_special_exception(
|
||||
exc_group: BaseExceptionGroup[
|
||||
Union[FinishedException, RejectedException, PausedException]
|
||||
]
|
||||
):
|
||||
nonlocal exc
|
||||
excs = list(flatten_exception_group(exc_group))
|
||||
if len(excs) > 1:
|
||||
logger.warning(
|
||||
"Multiple session control exceptions occurred. "
|
||||
"NoneBot will choose the proper one."
|
||||
)
|
||||
finished_exc = next(
|
||||
(e for e in excs if isinstance(e, FinishedException)),
|
||||
None,
|
||||
)
|
||||
rejected_exc = next(
|
||||
(e for e in excs if isinstance(e, RejectedException)),
|
||||
None,
|
||||
)
|
||||
paused_exc = next(
|
||||
(e for e in excs if isinstance(e, PausedException)),
|
||||
None,
|
||||
)
|
||||
exc = finished_exc or rejected_exc or paused_exc
|
||||
elif isinstance(
|
||||
excs[0], (FinishedException, RejectedException, PausedException)
|
||||
):
|
||||
exc = excs[0]
|
||||
|
||||
with catch(
|
||||
{
|
||||
(
|
||||
FinishedException,
|
||||
RejectedException,
|
||||
PausedException,
|
||||
): _handle_special_exception
|
||||
}
|
||||
):
|
||||
await self.simple_run(bot, event, state, stack, dependency_cache)
|
||||
|
||||
except RejectedException:
|
||||
if isinstance(exc, FinishedException):
|
||||
pass
|
||||
elif isinstance(exc, RejectedException):
|
||||
await self.resolve_reject()
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
@@ -870,7 +922,7 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except PausedException:
|
||||
elif isinstance(exc, PausedException):
|
||||
type_ = await self.update_type(bot, event, stack, dependency_cache)
|
||||
permission = await self.update_permission(
|
||||
bot, event, stack, dependency_cache
|
||||
@@ -890,5 +942,3 @@ class Matcher(metaclass=MatcherMeta):
|
||||
default_type_updater=self.__class__._default_type_updater,
|
||||
default_permission_updater=self.__class__._default_permission_updater,
|
||||
)
|
||||
except FinishedException:
|
||||
pass
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from enum import Enum
|
||||
from typing_extensions import Self, get_args, override, get_origin
|
||||
from contextlib import AsyncExitStack, contextmanager, asynccontextmanager
|
||||
from typing import (
|
||||
@@ -13,8 +13,11 @@ from typing import (
|
||||
cast,
|
||||
)
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
||||
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.dependencies import Param, Dependent
|
||||
from nonebot.dependencies.utils import check_field_type
|
||||
from nonebot.compat import FieldInfo, ModelField, PydanticUndefined, extract_field_info
|
||||
@@ -93,6 +96,78 @@ def Depends(
|
||||
return DependsInner(dependency, use_cache=use_cache, validate=validate)
|
||||
|
||||
|
||||
class CacheState(str, Enum):
|
||||
"""子依赖缓存状态"""
|
||||
|
||||
PENDING = "PENDING"
|
||||
FINISHED = "FINISHED"
|
||||
|
||||
|
||||
class DependencyCache:
|
||||
"""子依赖结果缓存。
|
||||
|
||||
用于缓存子依赖的结果,以避免重复计算。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._state = CacheState.PENDING
|
||||
self._result: Any = None
|
||||
self._exception: Optional[BaseException] = None
|
||||
self._waiter = anyio.Event()
|
||||
|
||||
def done(self) -> bool:
|
||||
return self._state == CacheState.FINISHED
|
||||
|
||||
def result(self) -> Any:
|
||||
"""获取子依赖结果"""
|
||||
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Result is not ready")
|
||||
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
return self._result
|
||||
|
||||
def exception(self) -> Optional[BaseException]:
|
||||
"""获取子依赖异常"""
|
||||
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Result is not ready")
|
||||
|
||||
return self._exception
|
||||
|
||||
def set_result(self, result: Any) -> None:
|
||||
"""设置子依赖结果"""
|
||||
|
||||
if self._state != CacheState.PENDING:
|
||||
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||
|
||||
self._result = result
|
||||
self._state = CacheState.FINISHED
|
||||
self._waiter.set()
|
||||
|
||||
def set_exception(self, exception: BaseException) -> None:
|
||||
"""设置子依赖异常"""
|
||||
|
||||
if self._state != CacheState.PENDING:
|
||||
raise RuntimeError(f"Cache state invalid: {self._state}")
|
||||
|
||||
self._exception = exception
|
||||
self._state = CacheState.FINISHED
|
||||
self._waiter.set()
|
||||
|
||||
async def wait(self):
|
||||
"""等待子依赖结果"""
|
||||
await self._waiter.wait()
|
||||
if self._state != CacheState.FINISHED:
|
||||
raise RuntimeError("Invalid cache state")
|
||||
|
||||
if self._exception is not None:
|
||||
raise self._exception
|
||||
|
||||
return self._result
|
||||
|
||||
|
||||
class DependParam(Param):
|
||||
"""子依赖注入参数。
|
||||
|
||||
@@ -102,7 +177,7 @@ class DependParam(Param):
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, *args, dependent: Dependent, use_cache: bool, **kwargs: Any
|
||||
self, *args, dependent: Dependent[Any], use_cache: bool, **kwargs: Any
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.dependent = dependent
|
||||
@@ -114,7 +189,7 @@ class DependParam(Param):
|
||||
@classmethod
|
||||
def _from_field(
|
||||
cls,
|
||||
sub_dependent: Dependent,
|
||||
sub_dependent: Dependent[Any],
|
||||
use_cache: bool,
|
||||
validate: Union[bool, PydanticFieldInfo],
|
||||
) -> Self:
|
||||
@@ -190,21 +265,31 @@ class DependParam(Param):
|
||||
use_cache: bool = self.use_cache
|
||||
dependency_cache = {} if dependency_cache is None else dependency_cache
|
||||
|
||||
sub_dependent: Dependent = self.dependent
|
||||
sub_dependent = self.dependent
|
||||
call = cast(Callable[..., Any], sub_dependent.call)
|
||||
|
||||
# solve sub dependency with current cache
|
||||
exc: Optional[BaseExceptionGroup[SkippedException]] = None
|
||||
|
||||
def _handle_skipped(exc_group: BaseExceptionGroup[SkippedException]):
|
||||
nonlocal exc
|
||||
exc = exc_group
|
||||
|
||||
with catch({SkippedException: _handle_skipped}):
|
||||
sub_values = await sub_dependent.solve(
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if exc is not None:
|
||||
raise exc
|
||||
|
||||
# run dependency function
|
||||
task: asyncio.Task[Any]
|
||||
if use_cache and call in dependency_cache:
|
||||
return await dependency_cache[call]
|
||||
elif is_gen_callable(call) or is_async_gen_callable(call):
|
||||
return await dependency_cache[call].wait()
|
||||
|
||||
if is_gen_callable(call) or is_async_gen_callable(call):
|
||||
assert isinstance(
|
||||
stack, AsyncExitStack
|
||||
), "Generator dependency should be called in context"
|
||||
@@ -212,17 +297,28 @@ class DependParam(Param):
|
||||
cm = run_sync_ctx_manager(contextmanager(call)(**sub_values))
|
||||
else:
|
||||
cm = asynccontextmanager(call)(**sub_values)
|
||||
task = asyncio.create_task(stack.enter_async_context(cm))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
|
||||
target = stack.enter_async_context(cm)
|
||||
elif is_coroutine_callable(call):
|
||||
task = asyncio.create_task(call(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
target = call(**sub_values)
|
||||
else:
|
||||
task = asyncio.create_task(run_sync(call)(**sub_values))
|
||||
dependency_cache[call] = task
|
||||
return await task
|
||||
target = run_sync(call)(**sub_values)
|
||||
|
||||
dependency_cache[call] = cache = DependencyCache()
|
||||
try:
|
||||
result = await target
|
||||
except Exception as e:
|
||||
cache.set_exception(e)
|
||||
raise
|
||||
except BaseException as e:
|
||||
cache.set_exception(e)
|
||||
# remove cache when base exception occurs
|
||||
# e.g. CancelledError
|
||||
dependency_cache.pop(call, None)
|
||||
raise
|
||||
else:
|
||||
cache.set_result(result)
|
||||
return result
|
||||
|
||||
@override
|
||||
async def _check(self, **kwargs: Any) -> None:
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
from typing_extensions import Self
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Union, ClassVar, NoReturn, Optional
|
||||
|
||||
import anyio
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.utils import run_coro_with_catch
|
||||
from nonebot.exception import SkippedException
|
||||
@@ -70,22 +71,26 @@ class Permission:
|
||||
"""
|
||||
if not self.checkers:
|
||||
return True
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
|
||||
result = False
|
||||
|
||||
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||
nonlocal result
|
||||
# calculate the result first to avoid data racing
|
||||
is_passed = await run_coro_with_catch(
|
||||
checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
bot=bot, event=event, stack=stack, dependency_cache=dependency_cache
|
||||
),
|
||||
(SkippedException,),
|
||||
False,
|
||||
)
|
||||
for checker in self.checkers
|
||||
),
|
||||
)
|
||||
return any(results)
|
||||
result |= is_passed
|
||||
|
||||
async with anyio.create_task_group() as tg:
|
||||
for checker in self.checkers:
|
||||
tg.start_soon(_run_checker, checker)
|
||||
|
||||
return result
|
||||
|
||||
def __and__(self, other: object) -> NoReturn:
|
||||
raise RuntimeError("And operation between Permissions is not allowed.")
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import asyncio
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import Union, ClassVar, NoReturn, Optional
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import SkippedException
|
||||
from nonebot.typing import T_State, T_RuleChecker, T_DependencyCache
|
||||
@@ -71,22 +73,33 @@ class Rule:
|
||||
"""
|
||||
if not self.checkers:
|
||||
return True
|
||||
try:
|
||||
results = await asyncio.gather(
|
||||
*(
|
||||
checker(
|
||||
|
||||
result = True
|
||||
|
||||
def _handle_skipped_exception(
|
||||
exc_group: BaseExceptionGroup[SkippedException],
|
||||
) -> None:
|
||||
nonlocal result
|
||||
result = False
|
||||
|
||||
async def _run_checker(checker: Dependent[bool]) -> None:
|
||||
nonlocal result
|
||||
# calculate the result first to avoid data racing
|
||||
is_passed = await checker(
|
||||
bot=bot,
|
||||
event=event,
|
||||
state=state,
|
||||
stack=stack,
|
||||
dependency_cache=dependency_cache,
|
||||
)
|
||||
for checker in self.checkers
|
||||
)
|
||||
)
|
||||
except SkippedException:
|
||||
return False
|
||||
return all(results)
|
||||
result &= is_passed
|
||||
|
||||
with catch({SkippedException: _handle_skipped_exception}):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for checker in self.checkers:
|
||||
tg.start_soon(_run_checker, checker)
|
||||
|
||||
return result
|
||||
|
||||
def __and__(self, other: Optional[Union["Rule", T_RuleChecker]]) -> "Rule":
|
||||
if other is None:
|
||||
|
@@ -8,6 +8,8 @@ NoneBot 使用 [`loguru`][loguru] 来记录日志信息。
|
||||
[loguru]: https://github.com/Delgan/loguru
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 7
|
||||
description: nonebot.log 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块实现事件响应器的创建与运行,并提供一些快捷方法来帮助用户更好的与机器人进行对话。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.matcher 模块
|
||||
"""
|
||||
|
@@ -3,27 +3,36 @@
|
||||
NoneBot 内部处理并按优先级分发事件给所有事件响应器,提供了多个插槽以进行事件的预处理等。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.message 模块
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional
|
||||
|
||||
import anyio
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.rule import TrieRule
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.matcher import Matcher, matchers
|
||||
from nonebot.utils import escape_tag, run_coro_with_catch
|
||||
from nonebot.exception import (
|
||||
NoLogException,
|
||||
StopPropagation,
|
||||
IgnoredException,
|
||||
SkippedException,
|
||||
)
|
||||
from nonebot.utils import (
|
||||
escape_tag,
|
||||
run_coro_with_catch,
|
||||
run_coro_with_shield,
|
||||
flatten_exception_group,
|
||||
)
|
||||
from nonebot.typing import (
|
||||
T_State,
|
||||
T_DependencyCache,
|
||||
@@ -123,6 +132,21 @@ def run_postprocessor(func: T_RunPostProcessor) -> T_RunPostProcessor:
|
||||
return func
|
||||
|
||||
|
||||
def _handle_ignored_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||
def _handle(exc_group: BaseExceptionGroup[IgnoredException]) -> None:
|
||||
logger.opt(colors=True).info(msg)
|
||||
|
||||
return _handle
|
||||
|
||||
|
||||
def _handle_exception(msg: str) -> Callable[[BaseExceptionGroup], None]:
|
||||
def _handle(exc_group: BaseExceptionGroup[Exception]) -> None:
|
||||
for exc in flatten_exception_group(exc_group):
|
||||
logger.opt(colors=True, exception=exc).error(msg)
|
||||
|
||||
return _handle
|
||||
|
||||
|
||||
async def _apply_event_preprocessors(
|
||||
bot: "Bot",
|
||||
event: "Event",
|
||||
@@ -150,10 +174,21 @@ async def _apply_event_preprocessors(
|
||||
if show_log:
|
||||
logger.debug("Running PreProcessors...")
|
||||
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
with catch(
|
||||
{
|
||||
IgnoredException: _handle_ignored_exception(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
),
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _event_preprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
@@ -163,23 +198,11 @@ async def _apply_event_preprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(
|
||||
f"Event {escape_tag(event.get_event_name())} is <b>ignored</b>"
|
||||
)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPreProcessors. "
|
||||
"Event ignored!</bg #f8bbd0></r>"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def _apply_event_postprocessors(
|
||||
bot: "Bot",
|
||||
@@ -205,10 +228,17 @@ async def _apply_event_postprocessors(
|
||||
if show_log:
|
||||
logger.debug("Running PostProcessors...")
|
||||
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
with catch(
|
||||
{
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _event_postprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
bot=bot,
|
||||
event=event,
|
||||
@@ -218,13 +248,6 @@ async def _apply_event_postprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _event_postprocessors
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running EventPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
|
||||
async def _apply_run_preprocessors(
|
||||
@@ -252,11 +275,24 @@ async def _apply_run_preprocessors(
|
||||
return True
|
||||
|
||||
# ensure matcher function can be correctly called
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
with (
|
||||
matcher.ensure_context(bot, event),
|
||||
catch(
|
||||
{
|
||||
IgnoredException: _handle_ignored_exception(
|
||||
f"{matcher} running is <b>cancelled</b>"
|
||||
),
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
),
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _run_preprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
matcher=matcher,
|
||||
bot=bot,
|
||||
@@ -267,21 +303,11 @@ async def _apply_run_preprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_preprocessors
|
||||
)
|
||||
)
|
||||
except IgnoredException:
|
||||
logger.opt(colors=True).info(f"{matcher} running is <b>cancelled</b>")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPreProcessors. "
|
||||
"Running cancelled!</bg #f8bbd0></r>"
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
async def _apply_run_postprocessors(
|
||||
bot: "Bot",
|
||||
@@ -304,11 +330,21 @@ async def _apply_run_postprocessors(
|
||||
if not _run_postprocessors:
|
||||
return
|
||||
|
||||
with matcher.ensure_context(bot, event):
|
||||
try:
|
||||
await asyncio.gather(
|
||||
*(
|
||||
run_coro_with_catch(
|
||||
with (
|
||||
matcher.ensure_context(bot, event),
|
||||
catch(
|
||||
{
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors"
|
||||
"</bg #f8bbd0></r>"
|
||||
)
|
||||
}
|
||||
),
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for proc in _run_postprocessors:
|
||||
tg.start_soon(
|
||||
run_coro_with_catch,
|
||||
proc(
|
||||
matcher=matcher,
|
||||
exception=exception,
|
||||
@@ -320,13 +356,6 @@ async def _apply_run_postprocessors(
|
||||
),
|
||||
(SkippedException,),
|
||||
)
|
||||
for proc in _run_postprocessors
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
"<r><bg #f8bbd0>Error when running RunPostProcessors</bg #f8bbd0></r>"
|
||||
)
|
||||
|
||||
|
||||
async def _check_matcher(
|
||||
@@ -423,8 +452,9 @@ async def _run_matcher(
|
||||
|
||||
exception = None
|
||||
|
||||
try:
|
||||
logger.debug(f"Running {matcher}")
|
||||
|
||||
try:
|
||||
await matcher.run(bot, event, state, stack, dependency_cache)
|
||||
except Exception as e:
|
||||
logger.opt(colors=True, exception=e).error(
|
||||
@@ -492,8 +522,7 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
|
||||
用法:
|
||||
```python
|
||||
import asyncio
|
||||
asyncio.create_task(handle_event(bot, event))
|
||||
driver.task_group.start_soon(handle_event, bot, event)
|
||||
```
|
||||
"""
|
||||
show_log = True
|
||||
@@ -528,6 +557,13 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
)
|
||||
|
||||
break_flag = False
|
||||
|
||||
def _handle_stop_propagation(exc_group: BaseExceptionGroup) -> None:
|
||||
nonlocal break_flag
|
||||
|
||||
break_flag = True
|
||||
logger.debug("Stop event propagation")
|
||||
|
||||
# iterate through all priority until stop propagation
|
||||
for priority in sorted(matchers.keys()):
|
||||
if break_flag:
|
||||
@@ -536,22 +572,29 @@ async def handle_event(bot: "Bot", event: "Event") -> None:
|
||||
if show_log:
|
||||
logger.debug(f"Checking for matchers in priority {priority}...")
|
||||
|
||||
pending_tasks = [
|
||||
check_and_run_matcher(
|
||||
matcher, bot, event, state.copy(), stack, dependency_cache
|
||||
)
|
||||
for matcher in matchers[priority]
|
||||
]
|
||||
results = await asyncio.gather(*pending_tasks, return_exceptions=True)
|
||||
for result in results:
|
||||
if not isinstance(result, Exception):
|
||||
if not (priority_matchers := matchers[priority]):
|
||||
continue
|
||||
if isinstance(result, StopPropagation):
|
||||
break_flag = True
|
||||
logger.debug("Stop event propagation")
|
||||
else:
|
||||
logger.opt(colors=True, exception=result).error(
|
||||
|
||||
with catch(
|
||||
{
|
||||
StopPropagation: _handle_stop_propagation,
|
||||
Exception: _handle_exception(
|
||||
"<r><bg #f8bbd0>Error when checking Matcher.</bg #f8bbd0></r>"
|
||||
),
|
||||
}
|
||||
):
|
||||
async with anyio.create_task_group() as tg:
|
||||
for matcher in priority_matchers:
|
||||
tg.start_soon(
|
||||
run_coro_with_shield,
|
||||
check_and_run_matcher(
|
||||
matcher,
|
||||
bot,
|
||||
event,
|
||||
state.copy(),
|
||||
stack,
|
||||
dependency_cache,
|
||||
),
|
||||
)
|
||||
|
||||
if show_log:
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块定义了依赖注入的各类参数。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 4
|
||||
description: nonebot.params 模块
|
||||
"""
|
||||
|
@@ -5,6 +5,8 @@
|
||||
只要有一个 `PermissionChecker` 检查结果为 `True` 时就会继续运行。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 6
|
||||
description: nonebot.permission 模块
|
||||
"""
|
||||
|
@@ -32,6 +32,8 @@
|
||||
- `PluginMetadata` => {ref}``PluginMetadata` <nonebot.plugin.model.PluginMetadata>`
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 0
|
||||
description: nonebot.plugin 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块定义插件加载接口。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 1
|
||||
description: nonebot.plugin.load 模块
|
||||
"""
|
||||
@@ -20,7 +22,7 @@ from . import _managers, get_plugin, _module_name_to_plugin_id
|
||||
try: # pragma: py-gte-311
|
||||
import tomllib # pyright: ignore[reportMissingImports]
|
||||
except ModuleNotFoundError: # pragma: py-lt-311
|
||||
import tomli as tomllib
|
||||
import tomli as tomllib # pyright: ignore[reportMissingImports]
|
||||
|
||||
|
||||
def load_plugin(module_path: Union[str, Path]) -> Optional[Plugin]:
|
||||
|
@@ -3,6 +3,8 @@
|
||||
参考: [import hooks](https://docs.python.org/3/reference/import.html#import-hooks), [PEP302](https://www.python.org/dev/peps/pep-0302/)
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.plugin.manager 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块定义插件相关信息。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 3
|
||||
description: nonebot.plugin.model 模块
|
||||
"""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""本模块定义事件响应器便携定义函数。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 2
|
||||
description: nonebot.plugin.on 模块
|
||||
"""
|
||||
@@ -114,7 +116,7 @@ def on(
|
||||
rule: Optional[Union[Rule, T_RuleChecker]] = None,
|
||||
permission: Optional[Union[Permission, T_PermissionChecker]] = None,
|
||||
*,
|
||||
handlers: Optional[list[Union[T_Handler, Dependent]]] = None,
|
||||
handlers: Optional[list[Union[T_Handler, Dependent[Any]]]] = None,
|
||||
temp: bool = False,
|
||||
expire_time: Optional[Union[datetime, timedelta]] = None,
|
||||
priority: int = 1,
|
||||
|
@@ -21,7 +21,7 @@ def on(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -32,7 +32,7 @@ def on_metaevent(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -43,7 +43,7 @@ def on_message(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -54,7 +54,7 @@ def on_notice(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -65,7 +65,7 @@ def on_request(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
*,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -78,7 +78,7 @@ def on_startswith(
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -91,7 +91,7 @@ def on_endswith(
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -104,7 +104,7 @@ def on_fullmatch(
|
||||
ignorecase: bool = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -116,7 +116,7 @@ def on_keyword(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -130,7 +130,7 @@ def on_command(
|
||||
force_whitespace: str | bool | None = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -144,7 +144,7 @@ def on_shell_command(
|
||||
parser: ArgumentParser | None = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -157,7 +157,7 @@ def on_regex(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -169,7 +169,7 @@ def on_type(
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
*,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -194,7 +194,7 @@ class CommandGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -209,7 +209,7 @@ class CommandGroup(_Group):
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
force_whitespace: str | bool | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -224,7 +224,7 @@ class CommandGroup(_Group):
|
||||
aliases: set[str | tuple[str, ...]] | None = ...,
|
||||
parser: ArgumentParser | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -239,7 +239,7 @@ class MatcherGroup(_Group):
|
||||
type: str = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -252,7 +252,7 @@ class MatcherGroup(_Group):
|
||||
type: str = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -264,7 +264,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -276,7 +276,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -288,7 +288,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -300,7 +300,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -314,7 +314,7 @@ class MatcherGroup(_Group):
|
||||
ignorecase: bool = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -328,7 +328,7 @@ class MatcherGroup(_Group):
|
||||
ignorecase: bool = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -342,7 +342,7 @@ class MatcherGroup(_Group):
|
||||
ignorecase: bool = ...,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -355,7 +355,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -370,7 +370,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -385,7 +385,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -399,7 +399,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
@@ -412,7 +412,7 @@ class MatcherGroup(_Group):
|
||||
*,
|
||||
rule: Rule | T_RuleChecker | None = ...,
|
||||
permission: Permission | T_PermissionChecker | None = ...,
|
||||
handlers: list[T_Handler | Dependent] | None = ...,
|
||||
handlers: list[T_Handler | Dependent[Any]] | None = ...,
|
||||
temp: bool = ...,
|
||||
expire_time: datetime | timedelta | None = ...,
|
||||
priority: int = ...,
|
||||
|
@@ -5,6 +5,8 @@
|
||||
只有当所有 `RuleChecker` 检查结果为 `True` 时继续运行。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 5
|
||||
description: nonebot.rule 模块
|
||||
"""
|
||||
|
@@ -6,6 +6,8 @@
|
||||
[`typing`](https://docs.python.org/3/library/typing.html)。
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 11
|
||||
description: nonebot.typing 模块
|
||||
"""
|
||||
@@ -13,17 +15,15 @@ FrontMatter:
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
import contextlib
|
||||
import typing as t
|
||||
import typing_extensions as t_ext
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing_extensions import ParamSpec, TypeAlias, get_args, override, get_origin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Task
|
||||
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.permission import Permission
|
||||
from nonebot.internal.params import DependencyCache
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
@@ -86,9 +86,7 @@ def all_literal_values(type_: type[t.Any]) -> list[t.Any]:
|
||||
|
||||
def origin_is_annotated(origin: t.Optional[type[t.Any]]) -> bool:
|
||||
"""判断是否是 Annotated 类型"""
|
||||
with contextlib.suppress(TypeError):
|
||||
return origin is not None and issubclass(origin, t_ext.Annotated)
|
||||
return False
|
||||
return origin is t_ext.Annotated
|
||||
|
||||
|
||||
NONE_TYPES = {None, type(None), t.Literal[None], t_ext.Literal[None]}
|
||||
@@ -259,5 +257,5 @@ T_PermissionUpdater: TypeAlias = _DependentCallable["Permission"]
|
||||
- MatcherParam: Matcher 对象
|
||||
- DefaultParam: 带有默认值的参数
|
||||
"""
|
||||
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "Task[t.Any]"]
|
||||
T_DependencyCache: TypeAlias = dict[_DependentCallable[t.Any], "DependencyCache"]
|
||||
"""依赖缓存, 用于存储依赖函数的返回值"""
|
||||
|
@@ -1,27 +1,30 @@
|
||||
"""本模块包含了 NoneBot 的一些工具函数
|
||||
|
||||
FrontMatter:
|
||||
mdx:
|
||||
format: md
|
||||
sidebar_position: 8
|
||||
description: nonebot.utils 模块
|
||||
"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import asyncio
|
||||
import inspect
|
||||
import importlib
|
||||
import contextlib
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
from collections import deque
|
||||
from contextvars import copy_context
|
||||
from functools import wraps, partial
|
||||
from contextlib import AbstractContextManager, asynccontextmanager
|
||||
from typing_extensions import ParamSpec, get_args, override, get_origin
|
||||
from collections.abc import Mapping, Sequence, Coroutine, AsyncGenerator
|
||||
from typing import Any, Union, Generic, TypeVar, Callable, Optional, overload
|
||||
from collections.abc import Mapping, Sequence, Coroutine, Generator, AsyncGenerator
|
||||
|
||||
import anyio
|
||||
import anyio.to_thread
|
||||
from pydantic import BaseModel
|
||||
from exceptiongroup import BaseExceptionGroup, catch
|
||||
|
||||
from nonebot.log import logger
|
||||
from nonebot.typing import (
|
||||
@@ -37,6 +40,7 @@ R = TypeVar("R")
|
||||
T = TypeVar("T")
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
E = TypeVar("E", bound=BaseException)
|
||||
|
||||
|
||||
def escape_tag(s: str) -> str:
|
||||
@@ -176,11 +180,9 @@ def run_sync(call: Callable[P, R]) -> Callable[P, Coroutine[None, None, R]]:
|
||||
|
||||
@wraps(call)
|
||||
async def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
loop = asyncio.get_running_loop()
|
||||
pfunc = partial(call, *args, **kwargs)
|
||||
context = copy_context()
|
||||
result = await loop.run_in_executor(None, partial(context.run, pfunc))
|
||||
return result
|
||||
return await anyio.to_thread.run_sync(
|
||||
partial(call, *args, **kwargs), abandon_on_cancel=True
|
||||
)
|
||||
|
||||
return _wrapper
|
||||
|
||||
@@ -232,12 +234,36 @@ async def run_coro_with_catch(
|
||||
协程的返回值或发生异常时的指定值
|
||||
"""
|
||||
|
||||
try:
|
||||
with catch({exc: lambda exc_group: None}):
|
||||
return await coro
|
||||
except exc:
|
||||
|
||||
return return_on_err
|
||||
|
||||
|
||||
async def run_coro_with_shield(coro: Coroutine[Any, Any, T]) -> T:
|
||||
"""运行协程并在取消时屏蔽取消异常。
|
||||
|
||||
参数:
|
||||
coro: 要运行的协程
|
||||
|
||||
返回:
|
||||
协程的返回值
|
||||
"""
|
||||
|
||||
with anyio.CancelScope(shield=True):
|
||||
return await coro
|
||||
|
||||
|
||||
def flatten_exception_group(
|
||||
exc_group: BaseExceptionGroup[E],
|
||||
) -> Generator[E, None, None]:
|
||||
for exc in exc_group.exceptions:
|
||||
if isinstance(exc, BaseExceptionGroup):
|
||||
yield from flatten_exception_group(exc)
|
||||
else:
|
||||
yield exc
|
||||
|
||||
|
||||
def get_name(obj: Any) -> str:
|
||||
"""获取对象的名称"""
|
||||
if inspect.isfunction(obj) or inspect.isclass(obj):
|
||||
|
2890
poetry.lock
generated
2890
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "nonebot2"
|
||||
version = "2.3.2"
|
||||
version = "2.4.0"
|
||||
description = "An asynchronous python bot framework."
|
||||
authors = ["yanyongyu <yyy@nonebot.dev>"]
|
||||
license = "MIT"
|
||||
@@ -22,12 +22,14 @@ include = ["nonebot/py.typed"]
|
||||
[tool.poetry.urls]
|
||||
"Bug Tracker" = "https://github.com/nonebot/nonebot2/issues"
|
||||
"Changelog" = "https://nonebot.dev/changelog"
|
||||
"Funding" = "https://afdian.net/@nonebot"
|
||||
"Funding" = "https://afdian.com/@nonebot"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
yarl = "^1.7.2"
|
||||
anyio = "^4.4.0"
|
||||
pygtrie = "^2.4.1"
|
||||
exceptiongroup = "^1.2.2"
|
||||
loguru = ">=0.6.0,<1.0.0"
|
||||
python-dotenv = ">=0.21.0,<2.0.0"
|
||||
typing-extensions = ">=4.4.0,<5.0.0"
|
||||
@@ -44,7 +46,7 @@ uvicorn = { version = ">=0.20.0,<1.0.0", extras = [
|
||||
], optional = true }
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.4.0"
|
||||
ruff = "^0.7.0"
|
||||
isort = "^5.10.1"
|
||||
black = "^24.0.0"
|
||||
nonemoji = "^0.1.2"
|
||||
@@ -65,7 +67,6 @@ fastapi = ["fastapi", "uvicorn"]
|
||||
all = ["fastapi", "quart", "aiohttp", "httpx", "websockets", "uvicorn"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "strict"
|
||||
addopts = "--cov=nonebot --cov-report=term-missing"
|
||||
filterwarnings = ["error", "ignore::DeprecationWarning"]
|
||||
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from functools import wraps
|
||||
from collections.abc import Generator
|
||||
from typing_extensions import ParamSpec
|
||||
from typing import TYPE_CHECKING, TypeVar, Callable
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
@@ -20,6 +22,9 @@ os.environ["CONFIG_OVERRIDE"] = "new"
|
||||
if TYPE_CHECKING:
|
||||
from nonebot.plugin import Plugin
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
collect_ignore = ["plugins/", "dynamic/", "bad_plugins/"]
|
||||
|
||||
|
||||
@@ -38,14 +43,36 @@ def load_driver(request: pytest.FixtureRequest) -> Driver:
|
||||
return DriverClass(Env(environment=global_driver.env), global_driver.config)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", params=[pytest.param("asyncio"), pytest.param("trio")])
|
||||
def anyio_backend(request: pytest.FixtureRequest):
|
||||
return request.param
|
||||
|
||||
|
||||
def run_once(func: Callable[P, R]) -> Callable[P, R]:
|
||||
result = ...
|
||||
|
||||
@wraps(func)
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
nonlocal result
|
||||
if result is not Ellipsis:
|
||||
return result
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_plugin(nonebug_init: None) -> set["Plugin"]:
|
||||
@run_once
|
||||
def load_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||
# preload global plugins
|
||||
return nonebot.load_plugins(str(Path(__file__).parent / "plugins"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_builtin_plugin(nonebug_init: None) -> set["Plugin"]:
|
||||
@run_once
|
||||
def load_builtin_plugin(anyio_backend, nonebug_init: None) -> set["Plugin"]:
|
||||
# preload builtin plugins
|
||||
return nonebot.load_builtin_plugins("echo", "single_session")
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from typing import Annotated
|
||||
from dataclasses import dataclass
|
||||
|
||||
import anyio
|
||||
from pydantic import Field
|
||||
|
||||
from nonebot import on_message
|
||||
@@ -105,3 +106,26 @@ async def validate_field(x: int = Depends(lambda: "1", validate=Field(gt=0))):
|
||||
|
||||
async def validate_field_fail(x: int = Depends(lambda: "0", validate=Field(gt=0))):
|
||||
return x
|
||||
|
||||
|
||||
async def _dep():
|
||||
await anyio.sleep(1)
|
||||
return 1
|
||||
|
||||
|
||||
def _dep_mismatch():
|
||||
return 1
|
||||
|
||||
|
||||
async def cache_exception_func1(
|
||||
dep: int = Depends(_dep),
|
||||
mismatch: dict = Depends(_dep_mismatch),
|
||||
):
|
||||
raise RuntimeError("Never reach here")
|
||||
|
||||
|
||||
async def cache_exception_func2(
|
||||
dep: int = Depends(_dep),
|
||||
match: int = Depends(_dep_mismatch),
|
||||
):
|
||||
return dep
|
||||
|
@@ -17,7 +17,7 @@ from nonebot.drivers import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_adapter_connect(app: App, driver: Driver):
|
||||
last_connect_bot: Optional[Bot] = None
|
||||
last_disconnect_bot: Optional[Bot] = None
|
||||
@@ -45,7 +45,6 @@ async def test_adapter_connect(app: App, driver: Driver):
|
||||
assert bot.self_id not in adapter.bots
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -75,7 +74,7 @@ async def test_adapter_connect(app: App, driver: Driver):
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
async def test_adapter_server(driver: Driver):
|
||||
def test_adapter_server(driver: Driver):
|
||||
last_http_setup: Optional[HTTPServerSetup] = None
|
||||
last_ws_setup: Optional[WebSocketServerSetup] = None
|
||||
|
||||
@@ -112,7 +111,7 @@ async def test_adapter_server(driver: Driver):
|
||||
assert last_ws_setup is setup
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -159,7 +158,7 @@ async def test_adapter_http_client(driver: Driver):
|
||||
assert last_request is request
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@@ -7,7 +8,7 @@ from nonebot.adapters import Bot
|
||||
from nonebot.exception import MockApiException
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_call_api(app: App):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
@@ -23,7 +24,7 @@ async def test_bot_call_api(app: App):
|
||||
await bot.call_api("test")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_calling_api_hook_simple(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -49,7 +50,7 @@ async def test_bot_calling_api_hook_simple(app: App):
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_calling_api_hook_mock(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -76,7 +77,47 @@ async def test_bot_calling_api_hook_mock(app: App):
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_calling_api_hook_multi_mock(app: App):
|
||||
runned1: bool = False
|
||||
runned2: bool = False
|
||||
event = anyio.Event()
|
||||
|
||||
async def calling_api_hook1(bot: Bot, api: str, data: dict[str, Any]):
|
||||
nonlocal runned1
|
||||
runned1 = True
|
||||
event.set()
|
||||
|
||||
raise MockApiException(1)
|
||||
|
||||
async def calling_api_hook2(bot: Bot, api: str, data: dict[str, Any]):
|
||||
nonlocal runned2
|
||||
runned2 = True
|
||||
with anyio.fail_after(1):
|
||||
await event.wait()
|
||||
|
||||
raise MockApiException(2)
|
||||
|
||||
hooks = set()
|
||||
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
m.setattr(Bot, "_calling_api_hook", hooks)
|
||||
|
||||
Bot.on_calling_api(calling_api_hook1)
|
||||
Bot.on_calling_api(calling_api_hook2)
|
||||
|
||||
assert hooks == {calling_api_hook1, calling_api_hook2}
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
result = await bot.call_api("test")
|
||||
|
||||
assert runned1 is True
|
||||
assert runned2 is True
|
||||
assert result == 1
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_called_api_hook_simple(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -108,7 +149,7 @@ async def test_bot_called_api_hook_simple(app: App):
|
||||
assert result is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_called_api_hook_mock(app: App):
|
||||
runned: bool = False
|
||||
|
||||
@@ -150,3 +191,56 @@ async def test_bot_called_api_hook_mock(app: App):
|
||||
|
||||
assert runned is True
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_called_api_hook_multi_mock(app: App):
|
||||
runned1: bool = False
|
||||
runned2: bool = False
|
||||
event = anyio.Event()
|
||||
|
||||
async def called_api_hook1(
|
||||
bot: Bot,
|
||||
exception: Optional[Exception],
|
||||
api: str,
|
||||
data: dict[str, Any],
|
||||
result: Any,
|
||||
):
|
||||
nonlocal runned1
|
||||
runned1 = True
|
||||
event.set()
|
||||
|
||||
raise MockApiException(1)
|
||||
|
||||
async def called_api_hook2(
|
||||
bot: Bot,
|
||||
exception: Optional[Exception],
|
||||
api: str,
|
||||
data: dict[str, Any],
|
||||
result: Any,
|
||||
):
|
||||
nonlocal runned2
|
||||
runned2 = True
|
||||
with anyio.fail_after(1):
|
||||
await event.wait()
|
||||
|
||||
raise MockApiException(2)
|
||||
|
||||
hooks = set()
|
||||
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
m.setattr(Bot, "_called_api_hook", hooks)
|
||||
|
||||
Bot.on_called_api(called_api_hook1)
|
||||
Bot.on_called_api(called_api_hook2)
|
||||
|
||||
assert hooks == {called_api_hook1, called_api_hook2}
|
||||
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.should_call_api("test", {}, True)
|
||||
result = await bot.call_api("test")
|
||||
|
||||
assert runned1 is True
|
||||
assert runned2 is True
|
||||
assert result == 1
|
||||
|
@@ -25,7 +25,7 @@ async def _dependency() -> int:
|
||||
return 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_preprocessors", set())
|
||||
@@ -58,7 +58,7 @@ async def test_event_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "event_preprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_preprocessors", set())
|
||||
@@ -88,7 +88,7 @@ async def test_event_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPat
|
||||
assert not runned, "matcher should not runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event_preprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -132,7 +132,7 @@ async def test_event_preprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_event_postprocessors", set())
|
||||
@@ -165,7 +165,7 @@ async def test_event_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "event_postprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event_postprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -202,7 +202,7 @@ async def test_event_postprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_preprocessors", set())
|
||||
@@ -239,7 +239,7 @@ async def test_run_preprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "run_preprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_preprocessors", set())
|
||||
@@ -269,7 +269,7 @@ async def test_run_preprocessor_ignore(app: App, monkeypatch: pytest.MonkeyPatch
|
||||
assert not runned, "matcher should not runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run_preprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
@@ -313,7 +313,7 @@ async def test_run_preprocessor_exception(
|
||||
assert "RuntimeError: test" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(message, "_run_postprocessors", set())
|
||||
@@ -351,7 +351,7 @@ async def test_run_postprocessor(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned, "run_postprocessor should runned"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run_postprocessor_exception(
|
||||
app: App, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
):
|
||||
|
@@ -1,13 +1,14 @@
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Annotated
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from nonebot.compat import (
|
||||
DEFAULT_CONFIG,
|
||||
Required,
|
||||
FieldInfo,
|
||||
TypeAdapter,
|
||||
PydanticUndefined,
|
||||
model_dump,
|
||||
custom_validation,
|
||||
@@ -16,14 +17,12 @@ from nonebot.compat import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_config():
|
||||
def test_default_config():
|
||||
assert DEFAULT_CONFIG.get("extra") == "allow"
|
||||
assert DEFAULT_CONFIG.get("arbitrary_types_allowed") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_field_info():
|
||||
def test_field_info():
|
||||
# required should be convert to PydanticUndefined
|
||||
assert FieldInfo(Required).default is PydanticUndefined
|
||||
|
||||
@@ -31,8 +30,21 @@ async def test_field_info():
|
||||
assert FieldInfo(test="test").extra["test"] == "test"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_dump():
|
||||
def test_type_adapter():
|
||||
t = TypeAdapter(Annotated[int, FieldInfo(ge=1)])
|
||||
|
||||
assert t.validate_python(2) == 2
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
t.validate_python(0)
|
||||
|
||||
assert t.validate_json("2") == 2
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
t.validate_json("0")
|
||||
|
||||
|
||||
def test_model_dump():
|
||||
class TestModel(BaseModel):
|
||||
test1: int
|
||||
test2: int
|
||||
@@ -41,8 +53,7 @@ async def test_model_dump():
|
||||
assert model_dump(TestModel(test1=1, test2=2), exclude={"test1"}) == {"test2": 2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_validation():
|
||||
def test_custom_validation():
|
||||
called = []
|
||||
|
||||
@custom_validation
|
||||
@@ -69,8 +80,7 @@ async def test_custom_validation():
|
||||
assert called == [1, 2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_json():
|
||||
def test_validate_json():
|
||||
class TestModel(BaseModel):
|
||||
test1: int
|
||||
test2: str
|
||||
|
@@ -50,16 +50,14 @@ class ExampleWithoutDelimiter(Example):
|
||||
env_nested_delimiter = None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_no_env():
|
||||
def test_config_no_env():
|
||||
config = Example(_env_file=None)
|
||||
assert config.simple == ""
|
||||
with pytest.raises(AttributeError):
|
||||
config.common_config
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_with_env():
|
||||
def test_config_with_env():
|
||||
config = Example(_env_file=(".env", ".env.example"))
|
||||
assert config.simple == "simple"
|
||||
|
||||
@@ -102,8 +100,7 @@ async def test_config_with_env():
|
||||
config.other_nested_inner__b
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_error_env():
|
||||
def test_config_error_env():
|
||||
with pytest.MonkeyPatch().context() as m:
|
||||
m.setenv("COMPLEX", "not json")
|
||||
|
||||
@@ -111,8 +108,7 @@ async def test_config_error_env():
|
||||
Example(_env_file=(".env", ".env.example"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_config_without_delimiter():
|
||||
def test_config_without_delimiter():
|
||||
config = ExampleWithoutDelimiter()
|
||||
assert config.nested.a == 1
|
||||
assert config.nested.b == 0
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Any, Optional
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
import anyio
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
@@ -25,7 +25,7 @@ from nonebot.drivers import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver", [pytest.param("nonebot.drivers.none:Driver", id="none")], indirect=True
|
||||
)
|
||||
@@ -59,22 +59,22 @@ async def test_lifespan(driver: Driver):
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _shutdown1():
|
||||
assert shutdown_log == []
|
||||
assert shutdown_log == [2]
|
||||
shutdown_log.append(1)
|
||||
|
||||
@driver.on_shutdown
|
||||
async def _shutdown2():
|
||||
assert shutdown_log == [1]
|
||||
assert shutdown_log == []
|
||||
shutdown_log.append(2)
|
||||
|
||||
async with driver._lifespan:
|
||||
assert start_log == [1, 2]
|
||||
assert ready_log == [1, 2]
|
||||
|
||||
assert shutdown_log == [1, 2]
|
||||
assert shutdown_log == [2, 1]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -99,10 +99,10 @@ async def test_http_server(app: App, driver: Driver):
|
||||
assert response.status_code == 200
|
||||
assert response.text == "test"
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -155,10 +155,10 @@ async def test_websocket_server(app: App, driver: Driver):
|
||||
|
||||
await ws.close(code=1000)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -171,9 +171,10 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
|
||||
ws: Optional[WebSocket] = None
|
||||
ws_ready = asyncio.Event()
|
||||
ws_should_close = asyncio.Event()
|
||||
ws_ready = anyio.Event()
|
||||
ws_should_close = anyio.Event()
|
||||
|
||||
# create a background task before the ws connection established
|
||||
async def background_task():
|
||||
try:
|
||||
await ws_ready.wait()
|
||||
@@ -185,8 +186,6 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
finally:
|
||||
ws_should_close.set()
|
||||
|
||||
task = asyncio.create_task(background_task())
|
||||
|
||||
async def _handle_ws(websocket: WebSocket) -> None:
|
||||
nonlocal ws
|
||||
await websocket.accept()
|
||||
@@ -199,7 +198,9 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
ws_setup = WebSocketServerSetup(URL("/ws_test"), "ws_test", _handle_ws)
|
||||
driver.setup_websocket_server(ws_setup)
|
||||
|
||||
async with app.test_server(driver.asgi) as ctx:
|
||||
async with anyio.create_task_group() as tg, app.test_server(driver.asgi) as ctx:
|
||||
tg.start_soon(background_task)
|
||||
|
||||
client = ctx.get_client()
|
||||
|
||||
async with client.websocket_connect("/ws_test") as websocket:
|
||||
@@ -211,11 +212,10 @@ async def test_cross_context(app: App, driver: Driver):
|
||||
if not e.args or "websocket.close" not in str(e.args[0]):
|
||||
raise
|
||||
|
||||
await task
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -304,10 +304,10 @@ async def test_http_client(driver: Driver, server_url: URL):
|
||||
"test3": "test",
|
||||
}, "file parsing error"
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -419,10 +419,10 @@ async def test_http_client_session(driver: Driver, server_url: URL):
|
||||
"test3": "test",
|
||||
}, "file parsing error"
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"driver",
|
||||
[
|
||||
@@ -452,10 +452,9 @@ async def test_websocket_client(driver: Driver, server_url: URL):
|
||||
with pytest.raises(WebSocketClosed, match=r"code=1000"):
|
||||
await ws.receive()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("driver", "driver_type"),
|
||||
[
|
||||
@@ -472,11 +471,11 @@ async def test_websocket_client(driver: Driver, server_url: URL):
|
||||
],
|
||||
indirect=["driver"],
|
||||
)
|
||||
async def test_combine_driver(driver: Driver, driver_type: str):
|
||||
def test_combine_driver(driver: Driver, driver_type: str):
|
||||
assert driver.type == driver_type
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
conn_hooks: set[Dependent[Any]] = set()
|
||||
@@ -533,7 +532,7 @@ async def test_bot_connect_hook(app: App, driver: Driver):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await anyio.sleep(1)
|
||||
|
||||
if not conn_should_be_called:
|
||||
pytest.fail("on_bot_connect hook not called")
|
||||
|
@@ -4,7 +4,7 @@ from nonebug import App
|
||||
from utils import FakeMessage, FakeMessageSegment, make_fake_event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_echo(app: App):
|
||||
from nonebot.plugins.echo import echo
|
||||
|
||||
|
@@ -14,8 +14,7 @@ from nonebot import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init():
|
||||
def test_init():
|
||||
env = nonebot.get_driver().env
|
||||
assert env == "test"
|
||||
|
||||
@@ -35,31 +34,28 @@ async def test_init():
|
||||
assert config.not_nested == "some string"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_driver(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_get_driver(monkeypatch: pytest.MonkeyPatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(nonebot, "_driver", None)
|
||||
with pytest.raises(ValueError, match="initialized"):
|
||||
get_driver()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_asgi(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_get_asgi():
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
assert get_asgi() == driver.asgi
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_app(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_get_app():
|
||||
driver = get_driver()
|
||||
assert isinstance(driver, ReverseDriver)
|
||||
assert isinstance(driver, ASGIMixin)
|
||||
assert get_app() == driver.server_app
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
async with app.test_api() as ctx:
|
||||
adapter = ctx.create_adapter()
|
||||
@@ -74,8 +70,7 @@ async def test_get_adapter(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
get_adapter("not exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_run(monkeypatch: pytest.MonkeyPatch):
|
||||
runned = False
|
||||
|
||||
def mock_run(*args, **kwargs):
|
||||
@@ -93,8 +88,7 @@ async def test_run(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
assert runned
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
def test_get_bot(app: App, monkeypatch: pytest.MonkeyPatch):
|
||||
driver = get_driver()
|
||||
|
||||
with pytest.raises(ValueError, match="no bots"):
|
||||
|
@@ -12,8 +12,7 @@ from nonebot.permission import User, Permission
|
||||
from nonebot.message import _check_matcher, check_and_run_matcher
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matcher_info(app: App):
|
||||
def test_matcher_info(app: App):
|
||||
from plugins.matcher.matcher_info import matcher
|
||||
|
||||
assert issubclass(matcher, Matcher)
|
||||
@@ -43,7 +42,7 @@ async def test_matcher_info(app: App):
|
||||
assert matcher._source.lineno == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_check(app: App):
|
||||
async def falsy():
|
||||
return False
|
||||
@@ -87,7 +86,7 @@ async def test_matcher_check(app: App):
|
||||
assert await _check_matcher(test_rule_error, bot, event, {}) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_handle(app: App):
|
||||
from plugins.matcher.matcher_process import test_handle
|
||||
|
||||
@@ -102,7 +101,7 @@ async def test_matcher_handle(app: App):
|
||||
ctx.should_finished()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_got(app: App):
|
||||
from plugins.matcher.matcher_process import test_got
|
||||
|
||||
@@ -124,7 +123,7 @@ async def test_matcher_got(app: App):
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_receive(app: App):
|
||||
from plugins.matcher.matcher_process import test_receive
|
||||
|
||||
@@ -141,7 +140,7 @@ async def test_matcher_receive(app: App):
|
||||
ctx.should_paused()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_combine(app: App):
|
||||
from plugins.matcher.matcher_process import test_combine
|
||||
|
||||
@@ -164,7 +163,7 @@ async def test_matcher_combine(app: App):
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_preset(app: App):
|
||||
from plugins.matcher.matcher_process import test_preset
|
||||
|
||||
@@ -182,7 +181,7 @@ async def test_matcher_preset(app: App):
|
||||
ctx.receive_event(bot, event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_overload(app: App):
|
||||
from plugins.matcher.matcher_process import test_overload
|
||||
|
||||
@@ -196,7 +195,7 @@ async def test_matcher_overload(app: App):
|
||||
ctx.should_finished()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_destroy(app: App):
|
||||
from plugins.matcher.matcher_process import test_destroy
|
||||
|
||||
@@ -210,7 +209,7 @@ async def test_matcher_destroy(app: App):
|
||||
assert len(matchers[test_destroy.priority]) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_type_updater(app: App):
|
||||
from plugins.matcher.matcher_type import test_type_updater, test_custom_updater
|
||||
|
||||
@@ -231,7 +230,7 @@ async def test_type_updater(app: App):
|
||||
assert new_type == "custom"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_default_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
@@ -252,7 +251,7 @@ async def test_default_permission_updater(app: App):
|
||||
assert checker.perm is default_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_user_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
default_permission,
|
||||
@@ -274,7 +273,7 @@ async def test_user_permission_updater(app: App):
|
||||
assert checker.perm is default_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_custom_permission_updater(app: App):
|
||||
from plugins.matcher.matcher_permission import (
|
||||
new_permission,
|
||||
@@ -291,7 +290,7 @@ async def test_custom_permission_updater(app: App):
|
||||
assert new_perm is new_permission
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_run(app: App):
|
||||
with app.provider.context({}):
|
||||
assert not matchers
|
||||
@@ -322,11 +321,12 @@ async def test_run(app: App):
|
||||
assert len(matchers[0][0].handlers) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_temp(app: App):
|
||||
from plugins.matcher.matcher_expire import test_temp_matcher
|
||||
|
||||
event = make_fake_event(_type="test")()
|
||||
with app.provider.context({test_temp_matcher.priority: [test_temp_matcher]}):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_temp_matcher in matchers[test_temp_matcher.priority]
|
||||
@@ -334,25 +334,33 @@ async def test_temp(app: App):
|
||||
assert test_temp_matcher not in matchers[test_temp_matcher.priority]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_datetime_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import test_datetime_matcher
|
||||
|
||||
event = make_fake_event()()
|
||||
async with app.test_api() as ctx:
|
||||
with app.provider.context(
|
||||
{test_datetime_matcher.priority: [test_datetime_matcher]}
|
||||
):
|
||||
async with app.test_matcher(test_datetime_matcher) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_datetime_matcher in matchers[test_datetime_matcher.priority]
|
||||
await check_and_run_matcher(test_datetime_matcher, bot, event, {})
|
||||
assert test_datetime_matcher not in matchers[test_datetime_matcher.priority]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_timedelta_expire(app: App):
|
||||
from plugins.matcher.matcher_expire import test_timedelta_matcher
|
||||
|
||||
event = make_fake_event()()
|
||||
with app.provider.context(
|
||||
{test_timedelta_matcher.priority: [test_timedelta_matcher]}
|
||||
):
|
||||
async with app.test_api() as ctx:
|
||||
bot = ctx.create_bot()
|
||||
assert test_timedelta_matcher in matchers[test_timedelta_matcher.priority]
|
||||
await check_and_run_matcher(test_timedelta_matcher, bot, event, {})
|
||||
assert test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
|
||||
assert (
|
||||
test_timedelta_matcher not in matchers[test_timedelta_matcher.priority]
|
||||
)
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import pytest
|
||||
from nonebug import App
|
||||
|
||||
from nonebot.matcher import DEFAULT_PROVIDER_CLASS, matchers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manager(app: App):
|
||||
def test_manager(app: App):
|
||||
try:
|
||||
default_provider = matchers.provider
|
||||
matchers.set_provider(DEFAULT_PROVIDER_CLASS)
|
||||
|
@@ -2,6 +2,7 @@ import re
|
||||
|
||||
import pytest
|
||||
from nonebug import App
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
from nonebot.matcher import Matcher
|
||||
from nonebot.dependencies import Dependent
|
||||
@@ -36,7 +37,7 @@ from nonebot.consts import (
|
||||
UNKNOWN_PARAM = "Unknown parameter"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_depend(app: App):
|
||||
from plugins.param.param_depend import (
|
||||
ClassDependency,
|
||||
@@ -50,6 +51,8 @@ async def test_depend(app: App):
|
||||
annotated_depend,
|
||||
sub_type_mismatch,
|
||||
validate_field_fail,
|
||||
cache_exception_func1,
|
||||
cache_exception_func2,
|
||||
annotated_class_depend,
|
||||
annotated_multi_depend,
|
||||
annotated_prior_depend,
|
||||
@@ -90,36 +93,67 @@ async def test_depend(app: App):
|
||||
|
||||
assert runned == [1, 1, 1]
|
||||
|
||||
runned.clear()
|
||||
|
||||
async with app.test_dependent(
|
||||
annotated_class_depend, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.should_return(ClassDependency(x=1, y=2))
|
||||
|
||||
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012
|
||||
async with app.test_dependent(
|
||||
sub_type_mismatch, allow_types=[DependParam, BotParam]
|
||||
) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
async with app.test_dependent(validate, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(1)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
|
||||
async with app.test_dependent(validate_fail, allow_types=[DependParam]) as ctx:
|
||||
...
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
async with app.test_dependent(validate_field, allow_types=[DependParam]) as ctx:
|
||||
ctx.should_return(1)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
|
||||
async with app.test_dependent(
|
||||
validate_field_fail, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
...
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
# test cache reuse when exception raised
|
||||
dependency_cache = {}
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
|
||||
async with app.test_dependent(
|
||||
cache_exception_func1, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(dependency_cache=dependency_cache)
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
# dependency solve tasks should be shielded even if one of them raises an exception
|
||||
assert len(dependency_cache) == 2
|
||||
|
||||
async with app.test_dependent(
|
||||
cache_exception_func2, allow_types=[DependParam]
|
||||
) as ctx:
|
||||
ctx.pass_params(dependency_cache=dependency_cache)
|
||||
ctx.should_return(1)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bot(app: App):
|
||||
from plugins.param.param_bot import (
|
||||
FooBot,
|
||||
@@ -157,11 +191,14 @@ async def test_bot(app: App):
|
||||
ctx.pass_params(bot=bot)
|
||||
ctx.should_return(bot)
|
||||
|
||||
with pytest.raises(TypeMisMatch): # noqa: PT012
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info: # noqa: PT012
|
||||
async with app.test_dependent(sub_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot()
|
||||
ctx.pass_params(bot=bot)
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
async with app.test_dependent(union_bot, allow_types=[BotParam]) as ctx:
|
||||
bot = ctx.create_bot(base=FooBot)
|
||||
ctx.pass_params(bot=bot)
|
||||
@@ -181,7 +218,7 @@ async def test_bot(app: App):
|
||||
app.test_dependent(not_bot, allow_types=[BotParam])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_event(app: App):
|
||||
from plugins.param.param_event import (
|
||||
FooEvent,
|
||||
@@ -223,10 +260,13 @@ async def test_event(app: App):
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_fooevent)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
|
||||
async with app.test_dependent(sub_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_event)
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
async with app.test_dependent(union_event, allow_types=[EventParam]) as ctx:
|
||||
ctx.pass_params(event=fake_fooevent)
|
||||
ctx.should_return(fake_fooevent)
|
||||
@@ -267,7 +307,7 @@ async def test_event(app: App):
|
||||
ctx.should_return(fake_event.is_tome())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_state(app: App):
|
||||
from plugins.param.param_state import (
|
||||
state,
|
||||
@@ -418,7 +458,7 @@ async def test_state(app: App):
|
||||
ctx.should_return(fake_state[KEYWORD_KEY])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher(app: App):
|
||||
from plugins.param.param_matcher import (
|
||||
FooMatcher,
|
||||
@@ -457,10 +497,13 @@ async def test_matcher(app: App):
|
||||
ctx.pass_params(matcher=foo_matcher)
|
||||
ctx.should_return(foo_matcher)
|
||||
|
||||
with pytest.raises(TypeMisMatch):
|
||||
with pytest.raises((TypeMisMatch, BaseExceptionGroup)) as exc_info:
|
||||
async with app.test_dependent(sub_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=fake_matcher)
|
||||
|
||||
if isinstance(exc_info.value, BaseExceptionGroup):
|
||||
assert exc_info.group_contains(TypeMisMatch)
|
||||
|
||||
async with app.test_dependent(union_matcher, allow_types=[MatcherParam]) as ctx:
|
||||
ctx.pass_params(matcher=foo_matcher)
|
||||
ctx.should_return(foo_matcher)
|
||||
@@ -496,7 +539,7 @@ async def test_matcher(app: App):
|
||||
ctx.should_return(event_next)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_arg(app: App):
|
||||
from plugins.param.param_arg import (
|
||||
arg,
|
||||
@@ -548,7 +591,7 @@ async def test_arg(app: App):
|
||||
ctx.should_return(message.extract_plain_text())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_exception(app: App):
|
||||
from plugins.param.param_exception import exc, legacy_exc
|
||||
|
||||
@@ -562,7 +605,7 @@ async def test_exception(app: App):
|
||||
ctx.should_return(exception)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_default(app: App):
|
||||
from plugins.param.param_default import default
|
||||
|
||||
@@ -570,11 +613,10 @@ async def test_default(app: App):
|
||||
ctx.should_return(1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_priority():
|
||||
def test_priority():
|
||||
from plugins.param.priority import complex_priority
|
||||
|
||||
dependent = Dependent.parse(
|
||||
dependent = Dependent[None].parse(
|
||||
call=complex_priority,
|
||||
allow_types=[
|
||||
DependParam,
|
||||
|
@@ -22,7 +22,7 @@ from nonebot.permission import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_permission(app: App):
|
||||
async def falsy():
|
||||
return False
|
||||
@@ -54,7 +54,7 @@ async def test_permission(app: App):
|
||||
assert await Permission(truthy, skipped)(bot, event) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", True), ("notice", False)])
|
||||
async def test_message(type: str, expected: bool):
|
||||
dependent = next(iter(MESSAGE.checkers))
|
||||
@@ -66,7 +66,7 @@ async def test_message(type: str, expected: bool):
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("notice", True)])
|
||||
async def test_notice(type: str, expected: bool):
|
||||
dependent = next(iter(NOTICE.checkers))
|
||||
@@ -78,7 +78,7 @@ async def test_notice(type: str, expected: bool):
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(("type", "expected"), [("message", False), ("request", True)])
|
||||
async def test_request(type: str, expected: bool):
|
||||
dependent = next(iter(REQUEST.checkers))
|
||||
@@ -90,7 +90,7 @@ async def test_request(type: str, expected: bool):
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("type", "expected"), [("message", False), ("meta_event", True)]
|
||||
)
|
||||
@@ -104,7 +104,7 @@ async def test_metaevent(type: str, expected: bool):
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("type", "user_id", "expected"),
|
||||
[
|
||||
@@ -128,7 +128,7 @@ async def test_superuser(app: App, type: str, user_id: str, expected: bool):
|
||||
assert await dependent(bot=bot, event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("session_ids", "session_id", "expected"),
|
||||
[
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin():
|
||||
def test_get_plugin():
|
||||
# check simple plugin
|
||||
plugin = nonebot.get_plugin("export")
|
||||
assert plugin
|
||||
@@ -22,8 +20,7 @@ async def test_get_plugin():
|
||||
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin_by_module_name():
|
||||
def test_get_plugin_by_module_name():
|
||||
# check get plugin by exact module name
|
||||
plugin = nonebot.get_plugin_by_module_name("plugins.nested")
|
||||
assert plugin
|
||||
@@ -48,8 +45,7 @@ async def test_get_plugin_by_module_name():
|
||||
assert plugin.module_name == "plugins.nested.plugins.nested_subplugin"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_available_plugin():
|
||||
def test_get_available_plugin():
|
||||
old_managers = _managers.copy()
|
||||
_managers.clear()
|
||||
try:
|
||||
@@ -63,8 +59,7 @@ async def test_get_available_plugin():
|
||||
_managers.extend(old_managers)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_plugin_config():
|
||||
def test_get_plugin_config():
|
||||
class Config(BaseModel):
|
||||
plugin_config: int
|
||||
|
||||
|
@@ -1,15 +1,44 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from functools import wraps
|
||||
from dataclasses import asdict
|
||||
from typing import TypeVar, Callable
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
import pytest
|
||||
|
||||
import nonebot
|
||||
from nonebot.plugin import Plugin, PluginManager, _managers, inherit_supported_adapters
|
||||
from nonebot.plugin import (
|
||||
Plugin,
|
||||
PluginManager,
|
||||
_plugins,
|
||||
_managers,
|
||||
inherit_supported_adapters,
|
||||
)
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin():
|
||||
def _recover(func: Callable[P, R]) -> Callable[P, R]:
|
||||
|
||||
@wraps(func)
|
||||
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
origin_managers = _managers.copy()
|
||||
origin_plugins = _plugins.copy()
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
_managers.clear()
|
||||
_managers.extend(origin_managers)
|
||||
_plugins.clear()
|
||||
_plugins.update(origin_plugins)
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
@_recover
|
||||
def test_load_plugin():
|
||||
# check regular
|
||||
assert nonebot.load_plugin("dynamic.simple")
|
||||
|
||||
@@ -20,8 +49,7 @@ async def test_load_plugin():
|
||||
assert nonebot.load_plugin("some_plugin_not_exist") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]):
|
||||
def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[Plugin]):
|
||||
loaded_plugins = {
|
||||
plugin for plugin in nonebot.get_loaded_plugins() if not plugin.parent_plugin
|
||||
}
|
||||
@@ -44,8 +72,7 @@ async def test_load_plugins(load_plugin: set[Plugin], load_builtin_plugin: set[P
|
||||
PluginManager(search_path=["plugins"]).load_all_plugins()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_nested_plugin():
|
||||
def test_load_nested_plugin():
|
||||
parent_plugin = nonebot.get_plugin("nested")
|
||||
sub_plugin = nonebot.get_plugin("nested:nested_subplugin")
|
||||
sub_plugin2 = nonebot.get_plugin("nested:nested_subplugin2")
|
||||
@@ -57,16 +84,16 @@ async def test_load_nested_plugin():
|
||||
assert parent_plugin.sub_plugins == {sub_plugin, sub_plugin2}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_json():
|
||||
@_recover
|
||||
def test_load_json():
|
||||
nonebot.load_from_json("./plugins.json")
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
nonebot.load_from_json("./plugins.invalid.json")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_toml():
|
||||
@_recover
|
||||
def test_load_toml():
|
||||
nonebot.load_from_toml("./plugins.toml")
|
||||
|
||||
with pytest.raises(ValueError, match="Cannot find"):
|
||||
@@ -76,19 +103,20 @@ async def test_load_toml():
|
||||
nonebot.load_from_toml("./plugins.invalid.toml")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bad_plugin():
|
||||
@_recover
|
||||
def test_bad_plugin():
|
||||
nonebot.load_plugins("bad_plugins")
|
||||
|
||||
assert nonebot.get_plugin("bad_plugin") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
@_recover
|
||||
def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
def _patched_find(name: str):
|
||||
pytest.fail("require existing plugin should not call find_manager_by_name")
|
||||
|
||||
monkeypatch.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr("nonebot.plugin.load._find_manager_by_name", _patched_find)
|
||||
|
||||
# require use module name
|
||||
nonebot.require("plugins.export")
|
||||
@@ -97,19 +125,20 @@ async def test_require_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
nonebot.require("nested:nested_subplugin")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
m = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"])
|
||||
_managers.append(m)
|
||||
@_recover
|
||||
def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
pm = PluginManager(["dynamic.require_not_loaded"], ["dynamic/require_not_loaded/"])
|
||||
_managers.append(pm)
|
||||
num_managers = len(_managers)
|
||||
|
||||
origin_load = PluginManager.load_plugin
|
||||
|
||||
def _patched_load(self: PluginManager, name: str):
|
||||
assert self is m
|
||||
assert self is pm
|
||||
return origin_load(self, name)
|
||||
|
||||
monkeypatch.setattr(PluginManager, "load_plugin", _patched_load)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(PluginManager, "load_plugin", _patched_load)
|
||||
|
||||
# require standalone plugin
|
||||
nonebot.require("dynamic.require_not_loaded")
|
||||
@@ -120,8 +149,8 @@ async def test_require_not_loaded(monkeypatch: pytest.MonkeyPatch):
|
||||
assert len(_managers) == num_managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_declared():
|
||||
@_recover
|
||||
def test_require_not_declared():
|
||||
num_managers = len(_managers)
|
||||
|
||||
nonebot.require("dynamic.require_not_declared")
|
||||
@@ -130,14 +159,13 @@ async def test_require_not_declared():
|
||||
assert _managers[-1].plugins == {"dynamic.require_not_declared"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_require_not_found():
|
||||
@_recover
|
||||
def test_require_not_found():
|
||||
with pytest.raises(RuntimeError):
|
||||
nonebot.require("some_plugin_not_exist")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_plugin_metadata():
|
||||
def test_plugin_metadata():
|
||||
from plugins.metadata import Config, FakeAdapter
|
||||
|
||||
plugin = nonebot.get_plugin("metadata")
|
||||
@@ -157,8 +185,7 @@ async def test_plugin_metadata():
|
||||
assert plugin.metadata.get_supported_adapters() == {FakeAdapter}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inherit_supported_adapters_not_found():
|
||||
def test_inherit_supported_adapters_not_found():
|
||||
with pytest.raises(RuntimeError):
|
||||
inherit_supported_adapters("some_plugin_not_exist")
|
||||
|
||||
@@ -166,7 +193,6 @@ async def test_inherit_supported_adapters_not_found():
|
||||
inherit_supported_adapters("export")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("inherit_plugins", "expected"),
|
||||
[
|
||||
@@ -233,7 +259,7 @@ async def test_inherit_supported_adapters_not_found():
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_inherit_supported_adapters_combine(
|
||||
def test_inherit_supported_adapters_combine(
|
||||
inherit_plugins: tuple[str], expected: set[str]
|
||||
):
|
||||
assert inherit_supported_adapters(*inherit_plugins) == expected
|
||||
|
@@ -1,11 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from nonebot.plugin import PluginManager, _managers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_plugin_name():
|
||||
def test_load_plugin_name():
|
||||
m = PluginManager(plugins=["dynamic.manager"])
|
||||
try:
|
||||
_managers.append(m)
|
||||
|
||||
# load by plugin id
|
||||
@@ -15,3 +13,5 @@ async def test_load_plugin_name():
|
||||
assert module1
|
||||
assert module2
|
||||
assert module1 is module2
|
||||
finally:
|
||||
_managers.remove(m)
|
||||
|
@@ -18,7 +18,6 @@ from nonebot.rule import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("matcher_name", "pre_rule_factory", "has_permission"),
|
||||
[
|
||||
@@ -102,7 +101,7 @@ from nonebot.rule import (
|
||||
pytest.param("matcher_group_on_type", lambda e: IsTypeRule(e), True),
|
||||
],
|
||||
)
|
||||
async def test_on(
|
||||
def test_on(
|
||||
matcher_name: str,
|
||||
pre_rule_factory: Optional[Callable[[type[Event]], T_RuleChecker]],
|
||||
has_permission: bool,
|
||||
@@ -150,8 +149,7 @@ async def test_on(
|
||||
assert matcher.module_name == "plugins.plugin.matchers"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runtime_on():
|
||||
def test_runtime_on():
|
||||
import plugins.plugin.matchers as module
|
||||
from plugins.plugin.matchers import matcher_on_factory
|
||||
|
||||
|
@@ -49,7 +49,7 @@ from nonebot.rule import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_rule(app: App):
|
||||
async def falsy():
|
||||
return False
|
||||
@@ -81,7 +81,7 @@ async def test_rule(app: App):
|
||||
assert await Rule(truthy, skipped)(bot, event, {}) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_trie(app: App):
|
||||
TrieRule.add_prefix("/fake-prefix", TRIE_VALUE("/", ("fake-prefix",)))
|
||||
|
||||
@@ -146,7 +146,7 @@ async def test_trie(app: App):
|
||||
del TrieRule.prefix["/fake-prefix"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
@@ -186,7 +186,7 @@ async def test_startswith(
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
@@ -226,7 +226,7 @@ async def test_endswith(
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("msg", "ignorecase", "type", "text", "expected"),
|
||||
[
|
||||
@@ -266,7 +266,7 @@ async def test_fullmatch(
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("kws", "type", "text", "expected"),
|
||||
[
|
||||
@@ -298,7 +298,7 @@ async def test_keyword(
|
||||
assert await dependent(event=event, state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("cmds", "force_whitespace", "cmd", "whitespace", "arg_text", "expected"),
|
||||
[
|
||||
@@ -344,7 +344,7 @@ async def test_command(
|
||||
assert await dependent(state=state) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_shell_command():
|
||||
state: T_State
|
||||
CMD = ("test",)
|
||||
@@ -451,7 +451,7 @@ async def test_shell_command():
|
||||
assert state[SHELL_ARGS].status != 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("pattern", "type", "text", "expected", "matched"),
|
||||
[
|
||||
@@ -494,7 +494,7 @@ async def test_regex(
|
||||
assert result.span() == matched.span()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("expected", [True, False])
|
||||
async def test_to_me(expected: bool):
|
||||
test_to_me = to_me()
|
||||
@@ -507,7 +507,7 @@ async def test_to_me(expected: bool):
|
||||
assert await dependent(event=event) == expected
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_is_type():
|
||||
Event1 = make_fake_event()
|
||||
Event2 = make_fake_event()
|
||||
|
@@ -5,7 +5,7 @@ import pytest
|
||||
from utils import make_fake_event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.anyio
|
||||
async def test_matcher_mutex():
|
||||
from nonebot.plugins.single_session import matcher_mutex, _running_matcher
|
||||
|
||||
|
@@ -14,7 +14,7 @@ NoneBot2 是一个现代、跨平台、可扩展的 Python 聊天机器人框架
|
||||
|
||||
### 异步优先
|
||||
|
||||
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
NoneBot 基于 Python [asyncio](https://docs.python.org/zh-cn/3/library/asyncio.html) / [trio](https://trio.readthedocs.io/en/stable/) 编写,并在异步机制的基础上进行了一定程度的同步函数兼容。
|
||||
|
||||
### 完整的类型注解
|
||||
|
||||
|
@@ -77,6 +77,10 @@ NoneBot 为四种类型的事件响应器提供了五个基本的辅助函数:
|
||||
|
||||
## 内置响应规则
|
||||
|
||||
:::tip
|
||||
响应规则的使用方法可以参考 [深入 - 响应规则](../appendices/rule.md)。
|
||||
:::
|
||||
|
||||
### `startswith`
|
||||
|
||||
`startswith` 响应规则用于匹配消息纯文本部分的开头是否与指定字符串(或一系列字符串)相同。可选参数 `ignorecase` 用于指定是否忽略大小写,默认为 `False`。
|
||||
|
@@ -82,14 +82,16 @@ async def do_something(bot: Bot):
|
||||
|
||||
### 事件预处理
|
||||
|
||||
这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。
|
||||
这个钩子函数会在 NoneBot 接收到新的事件时运行。支持依赖注入,可以注入 `Bot` 对象、事件、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 会使 NoneBot 忽略该事件。
|
||||
|
||||
```python
|
||||
from nonebot.exception import IgnoredException
|
||||
from nonebot.message import event_preprocessor
|
||||
|
||||
@event_preprocessor
|
||||
async def do_something(event: Event):
|
||||
pass
|
||||
if not event.is_tome():
|
||||
raise IgnoredException("some reason")
|
||||
```
|
||||
|
||||
### 事件后处理
|
||||
@@ -106,14 +108,16 @@ async def do_something(event: Event):
|
||||
|
||||
### 运行预处理
|
||||
|
||||
这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。
|
||||
这个钩子函数会在 NoneBot 运行事件响应器前运行。支持依赖注入,可以注入 `Bot` 对象、事件、事件响应器、会话状态。在这个钩子函数内抛出 `nonebot.exception.IgnoredException` 也会使 NoneBot 忽略本次运行。
|
||||
|
||||
```python
|
||||
from nonebot.message import run_preprocessor
|
||||
from nonebot.exception import IgnoredException
|
||||
|
||||
@run_preprocessor
|
||||
async def do_something(event: Event, matcher: Matcher):
|
||||
pass
|
||||
if not event.is_tome():
|
||||
raise IgnoredException("some reason")
|
||||
```
|
||||
|
||||
### 运行后处理
|
||||
|
@@ -73,10 +73,12 @@ CUSTOM_CONFIG=config in dotenv
|
||||
同时,设置环境变量:
|
||||
|
||||
```bash
|
||||
# windows
|
||||
set CUSTOM_CONFIG "config in environment variables"
|
||||
# windows cmd
|
||||
set CUSTOM_CONFIG 'config in environment variables'
|
||||
# windows powershell
|
||||
$Env:CUSTOM_CONFIG='config in environment variables'
|
||||
# linux/macOS
|
||||
export CUSTOM_CONFIG="config in environment variables"
|
||||
export CUSTOM_CONFIG='config in environment variables'
|
||||
```
|
||||
|
||||
那最终 NoneBot 所读取的内容为环境变量中的内容,即 `config in environment variables`。
|
||||
@@ -164,7 +166,7 @@ COMMON_CONFIG=common config # 这个配置项在任何环境中都会被加载
|
||||
在生产环境中,可以通过设置环境变量 `ENVIRONMENT=prod` 来确保 NoneBot 读取正确的环境配置。
|
||||
:::
|
||||
|
||||
#### .env.{ENVIRONMENT} 文件
|
||||
#### .env.\{ENVIRONMENT\} 文件
|
||||
|
||||
`.env.{ENVIRONMENT}` 文件类似于预设,可以让我们在多套不同的配置方案中灵活切换,默认 NoneBot 会读取 `.env.prod` 配置。如果你使用了 `nb-cli` 创建 `simple` 项目,那么将含有两套预设配置:`.env.dev` 和 `.env.prod`。
|
||||
|
||||
@@ -176,7 +178,7 @@ nonebot.init(_env_file=".env.dev")
|
||||
|
||||
这将忽略在 `.env` 文件或环境变量中指定的 `ENVIRONMENT` 配置项。
|
||||
|
||||
## 读取配置项
|
||||
## 读取全局配置项
|
||||
|
||||
NoneBot 的全局配置对象可以通过 `driver` 获取,如:
|
||||
|
||||
@@ -196,7 +198,7 @@ superusers = config.superusers
|
||||
|
||||
## 插件配置
|
||||
|
||||
在一个涉及大量配置项的项目中,通过直接读取配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。
|
||||
在一个涉及大量配置项的项目中,通过直接读取全局配置项的方式显然并不高效。同时,由于额外的全局配置项没有预先定义,开发时编辑器将无法提示字段与类型,并且运行时没有对配置项直接进行合法性检查。那么就需要一种方式来规范定义插件配置项。
|
||||
|
||||
在 NoneBot 中,我们使用强大高效的 `pydantic` 来定义配置模型,这个模型可以被用于配置的读取和类型检查等。例如在 `weather` 插件目录中新建 `config.py` 来定义一个模型:
|
||||
|
||||
@@ -218,7 +220,7 @@ class Config(BaseModel):
|
||||
|
||||
在 `config.py` 中,我们定义了一个 `Config` 类,它继承自 `pydantic.BaseModel`,并定义了一些配置项。在 `Config` 类中,我们还定义了一个 `check_priority` 方法,它用于检查 `weather_command_priority` 配置项的合法性。更多关于 `pydantic` 的编写方式,可以参考 [pydantic 官方文档](https://docs.pydantic.dev/)。
|
||||
|
||||
在定义好配置模型后,我们可以在插件加载时获取全局配置,导入插件自身的配置模型并使用:
|
||||
在定义好配置模型后,我们可以在插件加载时通过配置模型获取插件配置:
|
||||
|
||||
```python {5,11} title=weather/__init__.py
|
||||
from nonebot import get_plugin_config
|
||||
@@ -295,8 +297,10 @@ DRIVER=~fastapi+~httpx+~websockets
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set DRIVER '~fastapi+~httpx+~websockets'
|
||||
# windows powershell
|
||||
$Env:DRIVER='~fastapi+~httpx+~websockets'
|
||||
# linux/macOS
|
||||
export DRIVER='~fastapi+~httpx+~websockets'
|
||||
```
|
||||
@@ -331,8 +335,10 @@ HOST=127.0.0.1
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set HOST '127.0.0.1'
|
||||
# windows powershell
|
||||
$Env:HOST='127.0.0.1'
|
||||
# linux/macOS
|
||||
export HOST='127.0.0.1'
|
||||
```
|
||||
@@ -367,8 +373,10 @@ PORT=8080
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set PORT '8080'
|
||||
# windows powershell
|
||||
$Env:PORT='8080'
|
||||
# linux/macOS
|
||||
export PORT='8080'
|
||||
```
|
||||
@@ -407,8 +415,10 @@ LOG_LEVEL=DEBUG
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set LOG_LEVEL 'DEBUG'
|
||||
# windows powershell
|
||||
$Env:LOG_LEVEL='DEBUG'
|
||||
# linux/macOS
|
||||
export LOG_LEVEL='DEBUG'
|
||||
```
|
||||
@@ -443,8 +453,10 @@ API_TIMEOUT=10.0
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set API_TIMEOUT '10.0'
|
||||
# windows powershell
|
||||
$Env:API_TIMEOUT='10.0'
|
||||
# linux/macOS
|
||||
export API_TIMEOUT='10.0'
|
||||
```
|
||||
@@ -479,8 +491,10 @@ SUPERUSERS=["123123123"]
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set SUPERUSERS '["123123123"]'
|
||||
# windows powershell
|
||||
$Env:SUPERUSERS='["123123123"]'
|
||||
# linux/macOS
|
||||
export SUPERUSERS='["123123123"]'
|
||||
```
|
||||
@@ -515,8 +529,10 @@ NICKNAME=["bot"]
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set NICKNAME '["bot"]'
|
||||
# windows powershell
|
||||
$Env:NICKNAME='["bot"]'
|
||||
# linux/macOS
|
||||
export NICKNAME='["bot"]'
|
||||
```
|
||||
@@ -554,9 +570,12 @@ COMMAND_SEP=[".", " "]
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set COMMAND_START '["/", ""]'
|
||||
set COMMAND_SEP '[".", " "]'
|
||||
# windows powershell
|
||||
$Env:COMMAND_START='["/", ""]'
|
||||
$Env:COMMAND_SEP='[".", " "]'
|
||||
# linux/macOS
|
||||
export COMMAND_START='["/", ""]'
|
||||
export COMMAND_SEP='[".", " "]'
|
||||
@@ -592,8 +611,10 @@ SESSION_EXPIRE_TIMEOUT=00:02:00
|
||||
<TabItem value="env" label="系统环境变量">
|
||||
|
||||
```bash
|
||||
# windows
|
||||
# windows cmd
|
||||
set SESSION_EXPIRE_TIMEOUT '00:02:00'
|
||||
# windows powershell
|
||||
$Env:SESSION_EXPIRE_TIMEOUT='00:02:00'
|
||||
# linux/macOS
|
||||
export SESSION_EXPIRE_TIMEOUT='00:02:00'
|
||||
```
|
||||
|
@@ -29,8 +29,9 @@ import Messenger from "@site/src/components/Messenger";
|
||||
|
||||
例如,我们可以在 `weather` 插件中添加一个超级用户可用的指令:
|
||||
|
||||
```python {2,8} title=weather/__init__.py
|
||||
```python {3,9} title=weather/__init__.py
|
||||
from typing import Tuple
|
||||
from nonebot.params import Command
|
||||
from nonebot.permission import SUPERUSER
|
||||
|
||||
manage = on_command(
|
||||
|
@@ -20,7 +20,11 @@ options:
|
||||
|
||||
`RuleChecker` 是一个返回值为 `bool` 类型的依赖函数,即 `RuleChecker` 支持依赖注入。我们可以根据上一节中添加的[配置项](./config.mdx#插件配置),在 `weather` 插件目录中编写一个响应规则:
|
||||
|
||||
```python {3,4} title=weather/__init__.py
|
||||
```python {7,8} title=weather/__init__.py
|
||||
from nonebot import get_plugin_config
|
||||
|
||||
from .config import Config
|
||||
|
||||
plugin_config = get_plugin_config(Config)
|
||||
|
||||
async def is_enable() -> bool:
|
||||
@@ -54,8 +58,11 @@ weather = on_command("天气", rule=rule)
|
||||
|
||||
在定义响应规则时,我们可以将规则进行细分,来更好地复用规则。而在使用时,我们需要合并多个规则。除了使用 `Rule` 对象来组合多个 `RuleChecker` 外,我们还可以对 `Rule` 对象进行合并。在原 `weather` 插件中,我们可以将 `rule=to_me()` 与 `rule=is_enable` 使用 `&` 运算符合并:
|
||||
|
||||
```python {10} title=weather/__init__.py
|
||||
```python {13} title=weather/__init__.py
|
||||
from nonebot.rule import to_me
|
||||
from nonebot import get_plugin_config
|
||||
|
||||
from .config import Config
|
||||
|
||||
plugin_config = get_plugin_config(Config)
|
||||
|
||||
@@ -66,7 +73,7 @@ weather = on_command(
|
||||
"天气",
|
||||
rule=to_me() & is_enable,
|
||||
aliases={"weather", "查天气"},
|
||||
priority=plugin_config.weather_command_priority
|
||||
priority=plugin_config.weather_command_priority,
|
||||
block=True,
|
||||
)
|
||||
```
|
||||
|
@@ -71,14 +71,14 @@ alc = Alconna(".rd{roll:int}")
|
||||
assert alc.parse(".rd123").header["roll"] == 123
|
||||
```
|
||||
|
||||
Bracket Header 类似 python 里的 f-string 写法,通过 "{}" 声明匹配类型
|
||||
Bracket Header 类似 python 里的 f-string 写法,通过 `"{}"` 声明匹配类型
|
||||
|
||||
"{}" 中的内容为 "name:type or pat":
|
||||
`"{}"` 中的内容为 "name:type or pat":
|
||||
|
||||
- "{}", "{:}" ⇔ "(.+)", 占位符
|
||||
- "{foo}" ⇔ "(?P<foo>.+)"
|
||||
- "{:\d+}" ⇔ "(\d+)"
|
||||
- "{foo:int}" ⇔ "(?P<foo>\d+)",其中 "int" 部分若能转为 `BasePattern` 则读取里面的表达式
|
||||
- `"{}"`, `"{:}"` ⇔ `"(.+)"`, 占位符
|
||||
- `"{foo}"` ⇔ `"(?P<foo>.+)"`
|
||||
- `"{:\d+}"` ⇔ `"(\d+)"`
|
||||
- `"{foo:int}"` ⇔ `"(?P<foo>\d+)"`,其中 `"int"` 部分若能转为 `BasePattern` 则读取里面的表达式
|
||||
|
||||
## 参数声明(Args)
|
||||
|
||||
@@ -321,7 +321,7 @@ opt2 = Option("--foo", default=OptionResult(value=False, args={"bar": 1}))
|
||||
- `keep_crlf`: 命令解析时是否保留换行字符
|
||||
- `compact`: 命令是否允许第一个参数紧随头部
|
||||
- `strict`: 命令是否严格匹配,若为 False 则未知参数将作为名为 $extra 的参数
|
||||
- `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 {...},parentheses 为 $(...)
|
||||
- `context_style`: 命令上下文插值的风格,None 为关闭,bracket 为 `{...}`,parentheses 为 `$(...)`
|
||||
- `extra`: 命令的自定义额外信息
|
||||
|
||||
元数据一定使用 `meta=...` 形式传入:
|
||||
|
@@ -96,7 +96,7 @@ class Other(Segment):
|
||||
|
||||
```
|
||||
|
||||
:::tips
|
||||
:::tip
|
||||
|
||||
或许你注意到了 `Segment` 上有一个 `children` 属性。
|
||||
|
||||
@@ -291,7 +291,7 @@ msg.extend([Text("text")])
|
||||
|
||||
这里额外说明 `UniMessage.template` 的拓展控制符
|
||||
|
||||
相比 `Message`,UniMessage 对于 {:XXX} 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
|
||||
相比 `Message`,UniMessage 对于 `{:XXX}` 做了另一类拓展。其能够识别例如 At(xxx, yyy) 或 Emoji(aaa, bbb)的字符串并执行
|
||||
|
||||
以 At(...) 为例:
|
||||
|
||||
@@ -305,7 +305,7 @@ UniMessage(At("user", "123"))
|
||||
UniMessage(At("user", "123"))
|
||||
```
|
||||
|
||||
而在 `AlconnaMatcher` 中,{:XXX} 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
|
||||
而在 `AlconnaMatcher` 中,`{:XXX}` 更进一步地提供了获取 `event` 和 `bot` 中的属性的功能:
|
||||
|
||||
```python title=在AlconnaMatcher中使用通用消息段的拓展控制符
|
||||
from arclet.alconna import Alconna, Args
|
||||
|
@@ -31,17 +31,17 @@ require("nonebot_plugin_localstore")
|
||||
import nonebot_plugin_localstore as store
|
||||
|
||||
# 获取插件缓存目录
|
||||
cache_dir = store.get_cache_dir("plugin_name")
|
||||
cache_dir = store.get_plugin_cache_dir()
|
||||
# 获取插件缓存文件
|
||||
cache_file = store.get_cache_file("plugin_name", "file_name")
|
||||
cache_file = store.get_plugin_cache_file("file_name")
|
||||
# 获取插件数据目录
|
||||
data_dir = store.get_data_dir("plugin_name")
|
||||
data_dir = store.get_plugin_data_dir()
|
||||
# 获取插件数据文件
|
||||
data_file = store.get_data_file("plugin_name", "file_name")
|
||||
data_file = store.get_plugin_data_file("file_name")
|
||||
# 获取插件配置目录
|
||||
config_dir = store.get_config_dir("plugin_name")
|
||||
config_dir = store.get_plugin_config_dir()
|
||||
# 获取插件配置文件
|
||||
config_file = store.get_config_file("plugin_name", "file_name")
|
||||
config_file = store.get_plugin_config_file("file_name")
|
||||
```
|
||||
|
||||
:::danger 警告
|
||||
@@ -53,9 +53,61 @@ config_file = store.get_config_file("plugin_name", "file_name")
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
data_file = store.get_data_file("plugin_name", "file_name")
|
||||
data_file = store.get_plugin_data_file("file_name")
|
||||
# 写入文件内容
|
||||
data_file.write_text("Hello World!")
|
||||
# 读取文件内容
|
||||
data = data_file.read_text()
|
||||
```
|
||||
|
||||
:::note 提示
|
||||
|
||||
对于嵌套插件,子插件的存储目录将位于父插件存储目录下。
|
||||
|
||||
:::
|
||||
|
||||
## 配置项
|
||||
|
||||
### localstore_cache_dir
|
||||
|
||||
自定义缓存目录
|
||||
|
||||
默认值:
|
||||
|
||||
- macOS: `~/Library/Caches/<AppName>`
|
||||
- Unix: `~/.cache/<AppName>` (XDG default)
|
||||
- Windows: `C:\Users\<username>\AppData\Local\<AppName>\Cache`
|
||||
|
||||
```dotenv
|
||||
LOCALSTORE_CACHE_DIR=/tmp/cache
|
||||
```
|
||||
|
||||
### localstore_data_dir
|
||||
|
||||
自定义数据目录
|
||||
|
||||
默认值:
|
||||
|
||||
- macOS: `~/Library/Application Support/<AppName>`
|
||||
- Unix: `~/.local/share/<AppName>` or in $XDG_DATA_HOME, if defined
|
||||
- Win XP (not roaming): `C:\Documents and Settings\<username>\Application Data\<AppName>`
|
||||
- Win 7 (not roaming): `C:\Users\<username>\AppData\Local\<AppName>`
|
||||
|
||||
```dotenv
|
||||
LOCALSTORE_DATA_DIR=/tmp/data
|
||||
```
|
||||
|
||||
### localstore_config_dir
|
||||
|
||||
自定义配置目录
|
||||
|
||||
默认值:
|
||||
|
||||
- macOS: same as user_data_dir
|
||||
- Unix: `~/.config/<AppName>`
|
||||
- Win XP (roaming): `C:\Documents and Settings\<username>\Local Settings\Application Data\<AppName>`
|
||||
- Win 7 (roaming): `C:\Users\<username>\AppData\Roaming\<AppName>`
|
||||
|
||||
```dotenv
|
||||
LOCALSTORE_CONFIG_DIR=/tmp/config
|
||||
```
|
||||
|
@@ -74,7 +74,17 @@ pip install pytest-asyncio
|
||||
|
||||
## 配置测试
|
||||
|
||||
在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容:
|
||||
在开始测试之前,我们需要对测试进行一些配置,以正确启动我们的机器人。
|
||||
|
||||
首先我们需要配置 pytest-asyncio,在 `pyproject.toml` 的 pytest 配置部分添加:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
```
|
||||
|
||||
然后,我们在 `tests` 目录下新建 `conftest.py` 文件,添加以下内容:
|
||||
|
||||
```python title=tests/conftest.py
|
||||
import pytest
|
||||
@@ -83,7 +93,7 @@ import nonebot
|
||||
from nonebot.adapters.console import Adapter as ConsoleAdapter
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def load_bot():
|
||||
async def after_nonebot_init(after_nonebot_init: None):
|
||||
# 加载适配器
|
||||
driver = nonebot.get_driver()
|
||||
driver.register_adapter(ConsoleAdapter)
|
||||
@@ -94,9 +104,10 @@ def load_bot():
|
||||
|
||||
这样,我们就可以在测试中使用机器人的插件了。通常,我们不需要自行初始化 NoneBot,NoneBug 已经为我们运行了 `nonebot.init()`。如果需要自定义 NoneBot 初始化的参数,我们可以在 `conftest.py` 中添加 `pytest_configure` 钩子函数。例如,我们可以修改 NoneBot 配置环境为 `test` 并从环境变量中输入配置:
|
||||
|
||||
```python {3,5,7-9} title=tests/conftest.py
|
||||
```python {4,6,8-10} title=tests/conftest.py
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from nonebug import NONEBOT_INIT_KWARGS
|
||||
|
||||
os.environ["ENVIRONMENT"] = "test"
|
||||
@@ -105,6 +116,16 @@ def pytest_configure(config: pytest.Config):
|
||||
config.stash[NONEBOT_INIT_KWARGS] = {"secret": os.getenv("INPUT_SECRET")}
|
||||
```
|
||||
|
||||
NoneBug 默认也会为我们管理 lifespan 的 startup 与 shutdown。如果不希望 NoneBug 管理 lifespan,你可以在 `pytest_configure` 里添加以下配置:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from nonebug import NONEBOT_START_LIFESPAN
|
||||
|
||||
def pytest_configure(config: pytest.Config):
|
||||
config.stash[NONEBOT_START_LIFESPAN] = False
|
||||
```
|
||||
|
||||
## 编写插件测试
|
||||
|
||||
在配置完成插件加载后,我们就可以在测试中使用插件了。NoneBug 通过 pytest fixture `app` 提供各种测试方法,我们可以在测试中使用它来测试插件。现在,我们创建一个测试脚本来测试[深入指南](../../appendices/session-control.mdx)中编写的天气插件。首先,我们先要导入我们需要的模块:
|
||||
|
@@ -1,13 +1,15 @@
|
||||
---
|
||||
sidebar_position: 0
|
||||
description: 开源软件供应链点亮计划 - 暑期 2021
|
||||
mdx:
|
||||
format: md
|
||||
---
|
||||
|
||||
# 暑期 2021
|
||||
|
||||
**开源软件供应链点亮计划 - 暑期 2021** 是**中国科学院软件研究所**与 **openEuler 社区**共同举办的一项面向高校学生的暑期活动,旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer.iscas.ac.cn/) 和 [帮助文档](https://summer.iscas.ac.cn/help/)。
|
||||
|
||||
NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 <contact@nonebot.dev> 联系我们。
|
||||
NoneBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学在上面给出的活动官网报名,或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
|
||||
|
||||
## NoneBot v1
|
||||
|
||||
|
@@ -1,13 +1,15 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
description: 开源之夏 - 暑期 2022
|
||||
mdx:
|
||||
format: md
|
||||
---
|
||||
|
||||
# 暑期 2022
|
||||
|
||||
**开源之夏 - 暑期 2022** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
|
||||
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 <contact@nonebot.dev> 联系我们。
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/#/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a/) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学加入 QQ 群 [737131827](https://jq.qq.com/?_wv=1027&k=PEgyGeEu) 或通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
|
||||
|
||||
## NoneBot2 命令行 CLI 交互体验升级
|
||||
|
||||
|
@@ -1,13 +1,15 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
description: 开源之夏 - 暑期 2023
|
||||
mdx:
|
||||
format: md
|
||||
---
|
||||
|
||||
# 暑期 2023
|
||||
|
||||
**开源之夏 - 暑期 2023** 是由**开源软件供应链点亮计划**发起、由**中国科学院软件研究所**与 **openEuler 社区**主办的一项面向高校学生的暑期活动,类似 Google Summer of Code(GSoC),旨在鼓励在校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
|
||||
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 <contact@nonebot.dev> 联系我们。
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
|
||||
|
||||
## NoneBot 项目管理图形化面板
|
||||
|
||||
|
@@ -1,13 +1,15 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
description: 开源之夏 - 暑期 2024
|
||||
mdx:
|
||||
format: md
|
||||
---
|
||||
|
||||
# 暑期 2024
|
||||
|
||||
**开源之夏 - 暑期 2024** 是**中国科学院软件研究所**发起的**开源软件供应链点亮计划**系列暑期活动,旨在鼓励高校学生积极参与开源软件的开发维护,促进优秀开源软件社区的蓬勃发展。活动联合各大开源社区,针对重要开源软件的开发与维护提供项目开发任务,并向全球高校学生开放报名。关于具体的活动规划、报名方式,请查看该活动的 [官网](https://summer-ospp.ac.cn/) 和 [帮助文档](https://summer-ospp.ac.cn/help/)。
|
||||
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 <contact@nonebot.dev> 联系我们。
|
||||
NoneBot 社区有幸作为开源社区 [参与](https://summer-ospp.ac.cn/org/orgdetail/e1fb5b8d-125a-4138-b756-25bd32c0a31a?lang=zh) 了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学通过 [contact@nonebot.dev](mailto:contact@nonebot.dev) 联系我们。
|
||||
|
||||
## NonePress 官网组件库更新与优化
|
||||
|
||||
|
@@ -107,4 +107,4 @@ if __name__ == "__main__":
|
||||
python bot.py
|
||||
```
|
||||
|
||||
如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。
|
||||
如果你后续使用了 `nb-cli` ,你仍可以使用 `nb run` 命令来运行机器人,`nb-cli` 会自动检测入口文件 `bot.py` 是否存在并运行。同时,你也可以使用 `nb run --reload` 来自动检测代码的更改并自动重新运行入口文件。
|
||||
|
@@ -51,8 +51,8 @@ from nonebot.rule import to_me
|
||||
weather = on_command("天气", rule=to_me(), aliases={"weather", "查天气"}, priority=10, block=True)
|
||||
```
|
||||
|
||||
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令,需要私聊或 `@bot` 时才会响应,优先级为 10 ,阻断事件传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
|
||||
这样,我们就获得了一个可以响应 `天气`、`weather`、`查天气` 三个命令的响应规则,需要私聊或 `@bot` 时才会响应,优先级为 10(越小越优先),阻断事件向后续优先级传播的事件响应器了。这些内容的意义和使用方法将会在后续的章节中一一介绍。
|
||||
|
||||
:::tip 提示
|
||||
需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶](../advanced/matcher.md)或编辑器的提示。
|
||||
需要注意的是,不同的辅助函数有不同的可选参数,在使用之前可以参考[事件响应器进阶 - 基本辅助函数](../advanced/matcher.md#基本辅助函数)或 [API 文档](../api/plugin/on.md#on)。
|
||||
:::
|
||||
|
@@ -1,317 +0,0 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
// color mode config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["colorMode"]} */
|
||||
const colorMode = {
|
||||
defaultMode: "light",
|
||||
respectPrefersColorScheme: true,
|
||||
};
|
||||
|
||||
// navbar config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["navbar"]} */
|
||||
const navbar = {
|
||||
title: "NoneBot",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
hideOnScroll: false,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "doc",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store/plugins",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "doc",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "doc",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "doc", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "doc", docId: "ospp/2024" },
|
||||
{ label: "商店", to: "/store/plugins" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// footer config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["footer"]} */
|
||||
const footer = {
|
||||
style: "light",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
{ label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://v1.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// prism config
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const lightCodeTheme = require("prism-react-renderer/themes/github");
|
||||
/** @type {import('prism-react-renderer').PrismTheme} */
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line import/order
|
||||
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
|
||||
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["prism"]} */
|
||||
const prism = {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
};
|
||||
|
||||
// algolia config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["algolia"]} */
|
||||
const algolia = {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
};
|
||||
|
||||
// nonepress config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig["nonepress"]} */
|
||||
const nonepress = {
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
navbar: {
|
||||
docsVersionDropdown: {
|
||||
dropdownItemsAfter: [
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// theme config
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').ThemeConfig} */
|
||||
const themeConfig = {
|
||||
colorMode,
|
||||
navbar,
|
||||
footer,
|
||||
prism,
|
||||
algolia,
|
||||
nonepress,
|
||||
};
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const siteConfig = {
|
||||
title: "NoneBot",
|
||||
tagline: "跨平台 Python 异步机器人框架",
|
||||
favicon: "icons/favicon.ico",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://nonebot.dev",
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "nonebot", // Usually your GitHub org/user name.
|
||||
projectName: "nonebot2", // Usually your repo name.
|
||||
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "zh-Hans",
|
||||
locales: ["zh-Hans"],
|
||||
},
|
||||
|
||||
headTags: [
|
||||
// 百度搜索资源平台
|
||||
// https://ziyuan.baidu.com/
|
||||
{
|
||||
tagName: "meta",
|
||||
attributes: {
|
||||
name: "baidu-site-verification",
|
||||
content: "codeva-0GTZpDnDrW",
|
||||
},
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
// 百度统计
|
||||
// https://tongji.baidu.com/
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6",
|
||||
async: true,
|
||||
},
|
||||
// 万维广告
|
||||
// https://wwads.cn/
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "https://cdn.wwads.cn/js/makemoney.js",
|
||||
async: true,
|
||||
},
|
||||
// uwu logo
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "/uwu.js",
|
||||
},
|
||||
],
|
||||
|
||||
presets: [
|
||||
[
|
||||
"@nullbot/docusaurus-preset-nonepress",
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
// Please change this to your repo.
|
||||
editUrl: "https://github.com/nonebot/nonebot2/edit/master/website/",
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
// exclude: [
|
||||
// "**/_*.{js,jsx,ts,tsx,md,mdx}",
|
||||
// "**/_*/**",
|
||||
// "**/*.test.{js,jsx,ts,tsx}",
|
||||
// "**/__tests__/**",
|
||||
// ],
|
||||
// async sidebarItemsGenerator({
|
||||
// isCategoryIndex: defaultCategoryIndexMatcher,
|
||||
// defaultSidebarItemsGenerator,
|
||||
// ...args
|
||||
// }) {
|
||||
// return defaultSidebarItemsGenerator({
|
||||
// ...args,
|
||||
// isCategoryIndex(doc) {
|
||||
// // disable category index convention for generated API docs
|
||||
// if (
|
||||
// doc.directories.length > 0 &&
|
||||
// doc.directories.at(-1) === "api"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// return defaultCategoryIndexMatcher(doc);
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
},
|
||||
// theme: {
|
||||
// customCss: require.resolve("./src/css/custom.css"),
|
||||
// },
|
||||
sitemap: {
|
||||
changefreq: "daily",
|
||||
priority: 0.5,
|
||||
},
|
||||
gtag: {
|
||||
trackingID: "G-MRS1GMZG0F",
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
plugins: [require("./src/plugins/webpack-plugin.cjs")],
|
||||
|
||||
themeConfig,
|
||||
};
|
||||
|
||||
module.exports = siteConfig;
|
353
website/docusaurus.config.ts
Normal file
353
website/docusaurus.config.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import type { Config } from "@docusaurus/types";
|
||||
import type { Options as ChangelogOptions } from "@nullbot/docusaurus-plugin-changelog";
|
||||
import type * as Preset from "@nullbot/docusaurus-preset-nonepress";
|
||||
import { themes } from "prism-react-renderer";
|
||||
|
||||
// By default, we use Docusaurus Faster
|
||||
// DOCUSAURUS_SLOWER=true is useful for benchmarking faster against slower
|
||||
// hyperfine --prepare 'yarn clear:website' --runs 3 'DOCUSAURUS_SLOWER=true yarn build:website:fast' 'yarn build:website:fast'
|
||||
const isSlower = process.env.DOCUSAURUS_SLOWER === "true";
|
||||
if (isSlower) {
|
||||
console.log("🐢 Using slower Docusaurus build");
|
||||
}
|
||||
|
||||
// color mode config
|
||||
const colorMode: Preset.ThemeConfig["colorMode"] = {
|
||||
defaultMode: "light",
|
||||
respectPrefersColorScheme: true,
|
||||
};
|
||||
|
||||
// navbar config
|
||||
const navbar: Preset.ThemeConfig["navbar"] = {
|
||||
title: "NoneBot",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
hideOnScroll: false,
|
||||
items: [
|
||||
{
|
||||
label: "指南",
|
||||
type: "docsMenu",
|
||||
category: "tutorial",
|
||||
},
|
||||
{
|
||||
label: "深入",
|
||||
type: "docsMenu",
|
||||
category: "appendices",
|
||||
},
|
||||
{
|
||||
label: "进阶",
|
||||
type: "docsMenu",
|
||||
category: "advanced",
|
||||
},
|
||||
{
|
||||
label: "API",
|
||||
type: "doc",
|
||||
docId: "api/index",
|
||||
},
|
||||
{
|
||||
label: "更多",
|
||||
type: "dropdown",
|
||||
to: "/store/plugins",
|
||||
items: [
|
||||
{
|
||||
label: "最佳实践",
|
||||
type: "doc",
|
||||
docId: "best-practice/scheduler",
|
||||
},
|
||||
{
|
||||
label: "开发者",
|
||||
type: "doc",
|
||||
docId: "developer/plugin-publishing",
|
||||
},
|
||||
{ label: "社区", type: "doc", docId: "community/contact" },
|
||||
{ label: "开源之夏", type: "doc", docId: "ospp/2024" },
|
||||
{ label: "商店", to: "/store/plugins" },
|
||||
{ label: "更新日志", to: "/changelog" },
|
||||
{ label: "论坛", href: "https://discussions.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// footer config
|
||||
const footer: Preset.ThemeConfig["footer"] = {
|
||||
style: "light",
|
||||
logo: {
|
||||
alt: "NoneBot",
|
||||
src: "logo.png",
|
||||
href: "/",
|
||||
target: "_self",
|
||||
height: 32,
|
||||
width: 32,
|
||||
},
|
||||
copyright: `Copyright © ${new Date().getFullYear()} NoneBot. All rights reserved.`,
|
||||
links: [
|
||||
{
|
||||
title: "Learn",
|
||||
items: [
|
||||
{ label: "Introduction", to: "/docs/" },
|
||||
{ label: "QuickStart", to: "/docs/quick-start" },
|
||||
{ label: "Changelog", to: "/changelog" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "NoneBot Team",
|
||||
items: [
|
||||
{
|
||||
label: "Homepage",
|
||||
href: "https://nonebot.dev",
|
||||
},
|
||||
{
|
||||
label: "NoneBot V1",
|
||||
href: "https://v1.nonebot.dev",
|
||||
},
|
||||
{ label: "NoneBot CLI", href: "https://cli.nonebot.dev" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Related",
|
||||
items: [
|
||||
{ label: "OneBot", href: "https://onebot.dev/" },
|
||||
{ label: "go-cqhttp", href: "https://docs.go-cqhttp.org/" },
|
||||
{ label: "Mirai", href: "https://mirai.mamoe.net/" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// prism config
|
||||
const lightCodeTheme = themes.github;
|
||||
const darkCodeTheme = themes.dracula;
|
||||
|
||||
const prism: Preset.ThemeConfig["prism"] = {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
additionalLanguages: ["docker", "ini"],
|
||||
};
|
||||
|
||||
// algolia config
|
||||
const algolia: Preset.ThemeConfig["algolia"] = {
|
||||
appId: "X0X5UACHZQ",
|
||||
apiKey: "ac03e1ac2bd0812e2ea38c0cc1ea38c5",
|
||||
indexName: "nonebot",
|
||||
contextualSearch: true,
|
||||
};
|
||||
|
||||
// nonepress config
|
||||
const nonepress: Preset.ThemeConfig["nonepress"] = {
|
||||
tailwindConfig: require("./tailwind.config"),
|
||||
navbar: {
|
||||
docsVersionDropdown: {
|
||||
dropdownItemsAfter: [
|
||||
{
|
||||
label: "1.x",
|
||||
href: "https://v1.nonebot.dev/",
|
||||
},
|
||||
],
|
||||
},
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
socialLinks: [
|
||||
{
|
||||
icon: ["fab", "github"],
|
||||
href: "https://github.com/nonebot/nonebot2",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "qq"],
|
||||
href: "https://jq.qq.com/?_wv=1027&k=5OFifDh",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "telegram"],
|
||||
href: "https://t.me/botuniverse",
|
||||
},
|
||||
{
|
||||
icon: ["fab", "discord"],
|
||||
href: "https://discord.gg/VKtE6Gdc4h",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// theme config
|
||||
const themeConfig: Preset.ThemeConfig = {
|
||||
colorMode,
|
||||
navbar,
|
||||
footer,
|
||||
prism,
|
||||
algolia,
|
||||
nonepress,
|
||||
};
|
||||
|
||||
export default async function createConfigAsync() {
|
||||
return {
|
||||
title: "NoneBot",
|
||||
tagline: "跨平台 Python 异步机器人框架",
|
||||
favicon: "icons/favicon.ico",
|
||||
|
||||
// Set the production url of your site here
|
||||
url: "https://nonebot.dev",
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: process.env.BASE_URL || "/",
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: "nonebot", // Usually your GitHub org/user name.
|
||||
projectName: "nonebot2", // Usually your repo name.
|
||||
|
||||
onBrokenLinks: "throw",
|
||||
onBrokenMarkdownLinks: "warn",
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: "zh-Hans",
|
||||
locales: ["zh-Hans"],
|
||||
},
|
||||
|
||||
headTags: [
|
||||
// 百度搜索资源平台
|
||||
// https://ziyuan.baidu.com/
|
||||
{
|
||||
tagName: "meta",
|
||||
attributes: {
|
||||
name: "baidu-site-verification",
|
||||
content: "codeva-0GTZpDnDrW",
|
||||
},
|
||||
},
|
||||
],
|
||||
scripts: [
|
||||
// 百度统计
|
||||
// https://tongji.baidu.com/
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "https://hm.baidu.com/hm.js?875efa50097818701ee681edd63eaac6",
|
||||
async: true,
|
||||
},
|
||||
// 万维广告
|
||||
// https://wwads.cn/
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "https://cdn.wwads.cn/js/makemoney.js",
|
||||
async: true,
|
||||
},
|
||||
// uwu logo
|
||||
{
|
||||
type: "text/javascript",
|
||||
charset: "UTF-8",
|
||||
src: "/uwu.js",
|
||||
},
|
||||
],
|
||||
|
||||
presets: [
|
||||
[
|
||||
"@nullbot/docusaurus-preset-nonepress",
|
||||
/** @type {import('@nullbot/docusaurus-preset-nonepress').Options} */
|
||||
{
|
||||
docs: {
|
||||
sidebarPath: require.resolve("./sidebars.js"),
|
||||
// Please change this to your repo.
|
||||
editUrl: "https://github.com/nonebot/nonebot2/edit/master/website/",
|
||||
showLastUpdateAuthor: true,
|
||||
showLastUpdateTime: true,
|
||||
// exclude: [
|
||||
// "**/_*.{js,jsx,ts,tsx,md,mdx}",
|
||||
// "**/_*/**",
|
||||
// "**/*.test.{js,jsx,ts,tsx}",
|
||||
// "**/__tests__/**",
|
||||
// ],
|
||||
// async sidebarItemsGenerator({
|
||||
// isCategoryIndex: defaultCategoryIndexMatcher,
|
||||
// defaultSidebarItemsGenerator,
|
||||
// ...args
|
||||
// }) {
|
||||
// return defaultSidebarItemsGenerator({
|
||||
// ...args,
|
||||
// isCategoryIndex(doc) {
|
||||
// // disable category index convention for generated API docs
|
||||
// if (
|
||||
// doc.directories.length > 0 &&
|
||||
// doc.directories.at(-1) === "api"
|
||||
// ) {
|
||||
// return false;
|
||||
// }
|
||||
// return defaultCategoryIndexMatcher(doc);
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
},
|
||||
// theme: {
|
||||
// customCss: require.resolve("./src/css/custom.css"),
|
||||
// },
|
||||
sitemap: {
|
||||
changefreq: "daily",
|
||||
priority: 0.5,
|
||||
},
|
||||
gtag: {
|
||||
trackingID: "G-MRS1GMZG0F",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
webpack: {
|
||||
jsLoader: (isServer) => ({
|
||||
loader: require.resolve("swc-loader"),
|
||||
options: {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: "typescript",
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
target: "es2017",
|
||||
},
|
||||
module: {
|
||||
type: isServer ? "commonjs" : "es6",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
plugins: [
|
||||
require("./src/plugins/webpack-plugin.ts"),
|
||||
[
|
||||
"@nullbot/docusaurus-plugin-changelog",
|
||||
{
|
||||
changelogPath: "src/changelog/changelog.md",
|
||||
changelogHeader: `description: Changelog
|
||||
toc_max_heading_level: 2
|
||||
sidebar_custom_props:
|
||||
sidebar_id: changelog`,
|
||||
} satisfies ChangelogOptions,
|
||||
],
|
||||
],
|
||||
|
||||
markdown: {
|
||||
mdx1Compat: {
|
||||
headingIds: true,
|
||||
},
|
||||
},
|
||||
|
||||
themeConfig,
|
||||
} satisfies Config;
|
||||
}
|
@@ -22,24 +22,27 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.4.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"@nullbot/docusaurus-preset-nonepress": "^2.1.2",
|
||||
"clsx": "^1.2.1",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"@docusaurus/core": "^3.5.2",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@nullbot/docusaurus-plugin-changelog": "^3.0.0",
|
||||
"@nullbot/docusaurus-preset-nonepress": "^3.0.0",
|
||||
"@swc/core": "^1.7.26",
|
||||
"clsx": "^2.0.0",
|
||||
"copy-text-to-clipboard": "^3.2.0",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^17.0.1",
|
||||
"react": "^18.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-use-pagination": "^2.0.1"
|
||||
"react-dom": "^18.0.0",
|
||||
"react-use-pagination": "^2.0.1",
|
||||
"swc-loader": "^0.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"@docusaurus/module-type-aliases": "^3.5.2",
|
||||
"@nullbot/docusaurus-tsconfig": "^3.0.0",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"asciinema-player": "^3.5.0",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "~5.5.2"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -54,6 +57,6 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
"node": ">=18.0"
|
||||
}
|
||||
}
|
||||
|
@@ -8,11 +8,15 @@
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
import path from "path";
|
||||
|
||||
// @ts-check
|
||||
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
|
||||
import { getChangelogItemsSync } from "@nullbot/docusaurus-plugin-changelog";
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
const changelogPath = path.join(__dirname, "src/changelog/changelog.md");
|
||||
const changelogItems = getChangelogItemsSync(changelogPath, 10);
|
||||
|
||||
const sidebars: SidebarsConfig = {
|
||||
tutorial: [
|
||||
{
|
||||
type: "category",
|
||||
@@ -133,6 +137,22 @@ const sidebars = {
|
||||
],
|
||||
},
|
||||
],
|
||||
changelog: [
|
||||
{
|
||||
type: "category",
|
||||
label: "更新日志",
|
||||
collapsible: false,
|
||||
items: changelogItems.map<{ type: "link"; label: string; href: string }>(
|
||||
(chunk, index) => ({
|
||||
type: "link",
|
||||
label: chunk[0]!.title,
|
||||
href: `/changelog/${
|
||||
index > 0 ? encodeURIComponent(chunk[0]!.title) : ""
|
||||
}`,
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
||||
export default sidebars;
|
@@ -5,6 +5,170 @@ toc_max_heading_level: 2
|
||||
|
||||
# 更新日志
|
||||
|
||||
## v2.4.0
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Feature: 跳过部分非必要的 task group 创建 [@yanyongyu](https://github.com/yanyongyu) ([#3095](https://github.com/nonebot/nonebot2/pull/3095))
|
||||
- Feature: 迁移至结构化并发框架 AnyIO [@yanyongyu](https://github.com/yanyongyu) ([#3053](https://github.com/nonebot/nonebot2/pull/3053))
|
||||
- Feature: 添加 websockets 驱动器 proxy 连接警告 [@shoucandanghehe](https://github.com/shoucandanghehe) ([#2916](https://github.com/nonebot/nonebot2/pull/2916))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: 修复结构化并发子依赖取消缓存问题 [@yanyongyu](https://github.com/yanyongyu) ([#3084](https://github.com/nonebot/nonebot2/pull/3084))
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- Docs: 新增 nonebug 新版启动需要的配置 [@yanyongyu](https://github.com/yanyongyu) ([#3087](https://github.com/nonebot/nonebot2/pull/3087))
|
||||
- Docs: 修复侧边栏滚动 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#3062](https://github.com/nonebot/nonebot2/pull/3062))
|
||||
- Docs: 升级到 Docusaurus V3 [@StarHeartHunt](https://github.com/StarHeartHunt) ([#2956](https://github.com/nonebot/nonebot2/pull/2956))
|
||||
- Docs: 修改文档示例代码与部分表述 [@yixinNB](https://github.com/yixinNB) ([#2797](https://github.com/nonebot/nonebot2/pull/2797))
|
||||
- Docs: 添加钩子函数 IgnoredException 用法 [@refparo](https://github.com/refparo) ([#2912](https://github.com/nonebot/nonebot2/pull/2912))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Plugin: 移除不再维护的插件 [@ssttkkl](https://github.com/ssttkkl) ([#3040](https://github.com/nonebot/nonebot2/pull/3040))
|
||||
- Plugin: 删除不再维护的 simplemusic hikarisearch 插件 [@MeetWq](https://github.com/MeetWq) ([#2933](https://github.com/nonebot/nonebot2/pull/2933))
|
||||
- Plugin: 删除插件 `nonebot-plugin-ntqq-restart` [@kanbereina](https://github.com/kanbereina) ([#2926](https://github.com/nonebot/nonebot2/pull/2926))
|
||||
- Adapter: 移除社区版 mirai 适配器 [@RF-Tar-Railt](https://github.com/RF-Tar-Railt) ([#2909](https://github.com/nonebot/nonebot2/pull/2909))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: Comfyui绘图插件 [@noneflow](https://github.com/noneflow) ([#3081](https://github.com/nonebot/nonebot2/pull/3081))
|
||||
- Plugin: 每日wife [@noneflow](https://github.com/noneflow) ([#3094](https://github.com/nonebot/nonebot2/pull/3094))
|
||||
- Plugin: nonebot_plugin_impart [@noneflow](https://github.com/noneflow) ([#3079](https://github.com/nonebot/nonebot2/pull/3079))
|
||||
- Plugin: Pix图库 [@noneflow](https://github.com/noneflow) ([#3083](https://github.com/nonebot/nonebot2/pull/3083))
|
||||
- Plugin: nonebot_plugin_partner_join [@noneflow](https://github.com/noneflow) ([#3051](https://github.com/nonebot/nonebot2/pull/3051))
|
||||
- Plugin: pong [@noneflow](https://github.com/noneflow) ([#3066](https://github.com/nonebot/nonebot2/pull/3066))
|
||||
- Plugin: Bot的消息也是消息 [@noneflow](https://github.com/noneflow) ([#3064](https://github.com/nonebot/nonebot2/pull/3064))
|
||||
- Plugin: BiliMusic Downloader [@noneflow](https://github.com/noneflow) ([#3046](https://github.com/nonebot/nonebot2/pull/3046))
|
||||
- Plugin: 防撤回 [@noneflow](https://github.com/noneflow) ([#3055](https://github.com/nonebot/nonebot2/pull/3055))
|
||||
- Plugin: nonebot_plugin_mai_arcade [@noneflow](https://github.com/noneflow) ([#3047](https://github.com/nonebot/nonebot2/pull/3047))
|
||||
- Plugin: DDNet 成绩查询 [@noneflow](https://github.com/noneflow) ([#3031](https://github.com/nonebot/nonebot2/pull/3031))
|
||||
- Plugin: 省流 [@noneflow](https://github.com/noneflow) ([#3052](https://github.com/nonebot/nonebot2/pull/3052))
|
||||
- Plugin: FishSpeechTTS [@noneflow](https://github.com/noneflow) ([#3050](https://github.com/nonebot/nonebot2/pull/3050))
|
||||
- Plugin: 语音点歌 [@noneflow](https://github.com/noneflow) ([#3037](https://github.com/nonebot/nonebot2/pull/3037))
|
||||
- Plugin: Gotify [@noneflow](https://github.com/noneflow) ([#3043](https://github.com/nonebot/nonebot2/pull/3043))
|
||||
- Plugin: 涩图插件 [@noneflow](https://github.com/noneflow) ([#3039](https://github.com/nonebot/nonebot2/pull/3039))
|
||||
- Plugin: boom [@noneflow](https://github.com/noneflow) ([#3017](https://github.com/nonebot/nonebot2/pull/3017))
|
||||
- Plugin: 恶魔轮盘赌 [@noneflow](https://github.com/noneflow) ([#3033](https://github.com/nonebot/nonebot2/pull/3033))
|
||||
- Plugin: 机厅 [@noneflow](https://github.com/noneflow) ([#3029](https://github.com/nonebot/nonebot2/pull/3029))
|
||||
- Plugin: PM帮助 [@noneflow](https://github.com/noneflow) ([#3023](https://github.com/nonebot/nonebot2/pull/3023))
|
||||
- Plugin: NailongRemove [@noneflow](https://github.com/noneflow) ([#2972](https://github.com/nonebot/nonebot2/pull/2972))
|
||||
- Plugin: 团购 [@noneflow](https://github.com/noneflow) ([#3027](https://github.com/nonebot/nonebot2/pull/3027))
|
||||
- Plugin: 真寻日报 [@noneflow](https://github.com/noneflow) ([#3021](https://github.com/nonebot/nonebot2/pull/3021))
|
||||
- Plugin: 运行状态 [@noneflow](https://github.com/noneflow) ([#3019](https://github.com/nonebot/nonebot2/pull/3019))
|
||||
- Plugin: 西工大翱翔门户成绩监控 [@noneflow](https://github.com/noneflow) ([#3013](https://github.com/nonebot/nonebot2/pull/3013))
|
||||
- Plugin: nb插件更新器 [@noneflow](https://github.com/noneflow) ([#3015](https://github.com/nonebot/nonebot2/pull/3015))
|
||||
- Plugin: 涩涩保存器 [@noneflow](https://github.com/noneflow) ([#2988](https://github.com/nonebot/nonebot2/pull/2988))
|
||||
- Plugin: nonebot_plugin_BFVsearch [@noneflow](https://github.com/noneflow) ([#3008](https://github.com/nonebot/nonebot2/pull/3008))
|
||||
- Plugin: lingyi_chat [@noneflow](https://github.com/noneflow) ([#3006](https://github.com/nonebot/nonebot2/pull/3006))
|
||||
- Plugin: ZXPM插件管理 [@noneflow](https://github.com/noneflow) ([#3003](https://github.com/nonebot/nonebot2/pull/3003))
|
||||
- Plugin: MinecraftWatcher [@noneflow](https://github.com/noneflow) ([#3010](https://github.com/nonebot/nonebot2/pull/3010))
|
||||
- Plugin: BF5_grouptools [@noneflow](https://github.com/noneflow) ([#3004](https://github.com/nonebot/nonebot2/pull/3004))
|
||||
- Plugin: lolinfo [@noneflow](https://github.com/noneflow) ([#2997](https://github.com/nonebot/nonebot2/pull/2997))
|
||||
- Plugin: osu! Match Monitor [@noneflow](https://github.com/noneflow) ([#2985](https://github.com/nonebot/nonebot2/pull/2985))
|
||||
- Plugin: Marsho AI插件 [@noneflow](https://github.com/noneflow) ([#2993](https://github.com/nonebot/nonebot2/pull/2993))
|
||||
- Plugin: nonechat [@noneflow](https://github.com/noneflow) ([#2990](https://github.com/nonebot/nonebot2/pull/2990))
|
||||
- Plugin: nonebot_plugin_SimpleToWrite [@noneflow](https://github.com/noneflow) ([#2995](https://github.com/nonebot/nonebot2/pull/2995))
|
||||
- Plugin: Beat Saber查分器 [@noneflow](https://github.com/noneflow) ([#2974](https://github.com/nonebot/nonebot2/pull/2974))
|
||||
- Plugin: githubmodels [@noneflow](https://github.com/noneflow) ([#2945](https://github.com/nonebot/nonebot2/pull/2945))
|
||||
- Plugin: 给我点颜色瞧瞧 [@noneflow](https://github.com/noneflow) ([#2984](https://github.com/nonebot/nonebot2/pull/2984))
|
||||
- Plugin: pjsk-helper [@noneflow](https://github.com/noneflow) ([#2980](https://github.com/nonebot/nonebot2/pull/2980))
|
||||
- Plugin: 趣味内容插件 [@noneflow](https://github.com/noneflow) ([#2981](https://github.com/nonebot/nonebot2/pull/2981))
|
||||
- Plugin: 计算器:游戏 [@noneflow](https://github.com/noneflow) ([#2976](https://github.com/nonebot/nonebot2/pull/2976))
|
||||
- Plugin: nonebot-plugin-yareminder [@noneflow](https://github.com/noneflow) ([#2964](https://github.com/nonebot/nonebot2/pull/2964))
|
||||
- Plugin: 批量撤回 [@noneflow](https://github.com/noneflow) ([#2966](https://github.com/nonebot/nonebot2/pull/2966))
|
||||
- Plugin: inspect [@noneflow](https://github.com/noneflow) ([#2971](https://github.com/nonebot/nonebot2/pull/2971))
|
||||
- Plugin: 通用信息 [@noneflow](https://github.com/noneflow) ([#2969](https://github.com/nonebot/nonebot2/pull/2969))
|
||||
- Plugin: SSE日志输出流 [@noneflow](https://github.com/noneflow) ([#2960](https://github.com/nonebot/nonebot2/pull/2960))
|
||||
- Plugin: WITFF [@noneflow](https://github.com/noneflow) ([#2955](https://github.com/nonebot/nonebot2/pull/2955))
|
||||
- Plugin: weather-rank [@noneflow](https://github.com/noneflow) ([#2949](https://github.com/nonebot/nonebot2/pull/2949))
|
||||
- Plugin: 二维码生成器 [@noneflow](https://github.com/noneflow) ([#2942](https://github.com/nonebot/nonebot2/pull/2942))
|
||||
- Plugin: 次元星辰 [@noneflow](https://github.com/noneflow) ([#2935](https://github.com/nonebot/nonebot2/pull/2935))
|
||||
- Plugin: nonebot-plugin-tarina-lang-turbo [@noneflow](https://github.com/noneflow) ([#2938](https://github.com/nonebot/nonebot2/pull/2938))
|
||||
- Plugin: 狼人杀 [@noneflow](https://github.com/noneflow) ([#2932](https://github.com/nonebot/nonebot2/pull/2932))
|
||||
- Plugin: 阿瓦隆 [@noneflow](https://github.com/noneflow) ([#2915](https://github.com/nonebot/nonebot2/pull/2915))
|
||||
- Plugin: 消音器 [@noneflow](https://github.com/noneflow) ([#2919](https://github.com/nonebot/nonebot2/pull/2919))
|
||||
- Plugin: 悠悠 [@noneflow](https://github.com/noneflow) ([#2928](https://github.com/nonebot/nonebot2/pull/2928))
|
||||
- Plugin: LLOneBot-Master [@noneflow](https://github.com/noneflow) ([#2925](https://github.com/nonebot/nonebot2/pull/2925))
|
||||
- Plugin: 无情的发图姬 [@noneflow](https://github.com/noneflow) ([#2923](https://github.com/nonebot/nonebot2/pull/2923))
|
||||
- Plugin: maimai DX 查分 [@noneflow](https://github.com/noneflow) ([#2921](https://github.com/nonebot/nonebot2/pull/2921))
|
||||
- Plugin: Minecraft查服 [@noneflow](https://github.com/noneflow) ([#2882](https://github.com/nonebot/nonebot2/pull/2882))
|
||||
- Plugin: lagrange [@noneflow](https://github.com/noneflow) ([#2898](https://github.com/nonebot/nonebot2/pull/2898))
|
||||
- Plugin: nekro-agent [@noneflow](https://github.com/noneflow) ([#2896](https://github.com/nonebot/nonebot2/pull/2896))
|
||||
- Plugin: nonebot_plugin_mute [@noneflow](https://github.com/noneflow) ([#2893](https://github.com/nonebot/nonebot2/pull/2893))
|
||||
- Plugin: LiteyukiBot(plugin) [@noneflow](https://github.com/noneflow) ([#2905](https://github.com/nonebot/nonebot2/pull/2905))
|
||||
- Plugin: 复读6 [@noneflow](https://github.com/noneflow) ([#2900](https://github.com/nonebot/nonebot2/pull/2900))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: CanrotBot [@noneflow](https://github.com/noneflow) ([#3086](https://github.com/nonebot/nonebot2/pull/3086))
|
||||
- Bot: 小安提Bot [@noneflow](https://github.com/noneflow) ([#3061](https://github.com/nonebot/nonebot2/pull/3061))
|
||||
|
||||
## v2.3.3
|
||||
|
||||
### 🚀 新功能
|
||||
|
||||
- Feature: 优化依赖注入在 pydantic v2 下的性能 [@yanyongyu](https://github.com/yanyongyu) ([#2870](https://github.com/nonebot/nonebot2/pull/2870))
|
||||
- Feature: 添加遗漏的类型标注 [@yanyongyu](https://github.com/yanyongyu) ([#2856](https://github.com/nonebot/nonebot2/pull/2856))
|
||||
|
||||
### 🐛 Bug 修复
|
||||
|
||||
- Fix: 错误的类型标注和 annotated 处理 [@yanyongyu](https://github.com/yanyongyu) ([#2828](https://github.com/nonebot/nonebot2/pull/2828))
|
||||
|
||||
### 📝 文档
|
||||
|
||||
- Docs: 添加 Windows Powershell 设置环境变量方法 [@LeoQuote](https://github.com/LeoQuote) ([#2874](https://github.com/nonebot/nonebot2/pull/2874))
|
||||
- Docs: 更新 localstore 插件文档 [@yanyongyu](https://github.com/yanyongyu) ([#2871](https://github.com/nonebot/nonebot2/pull/2871))
|
||||
|
||||
### 💫 杂项
|
||||
|
||||
- Plugin: 修改插件 system-command 信息 [@tkgs0](https://github.com/tkgs0) ([#2862](https://github.com/nonebot/nonebot2/pull/2862))
|
||||
- Plugin: 修改 nonebot-plugin-fishing 插件作者 [@ALittleBot](https://github.com/ALittleBot) ([#2854](https://github.com/nonebot/nonebot2/pull/2854))
|
||||
- Bot: 更新 Minecraft QQBot 信息 [@Lonely-Sails](https://github.com/Lonely-Sails) ([#2838](https://github.com/nonebot/nonebot2/pull/2838))
|
||||
- Plugin: 移除 kanonbot 插件 [@SuperGuGuGu](https://github.com/SuperGuGuGu) ([#2819](https://github.com/nonebot/nonebot2/pull/2819))
|
||||
- Plugin: 更新插件 sparkapi 信息 [@CCLMSY](https://github.com/CCLMSY) ([#2812](https://github.com/nonebot/nonebot2/pull/2812))
|
||||
- Plugin: 修改插件 miragetank \& charpic 信息 [@1umine](https://github.com/1umine) ([#2807](https://github.com/nonebot/nonebot2/pull/2807))
|
||||
|
||||
### 🍻 插件发布
|
||||
|
||||
- Plugin: nonebot-plugin-wait-a-minute [@noneflow](https://github.com/noneflow) ([#2902](https://github.com/nonebot/nonebot2/pull/2902))
|
||||
- Plugin: 你看我像 [@noneflow](https://github.com/noneflow) ([#2895](https://github.com/nonebot/nonebot2/pull/2895))
|
||||
- Plugin: dify插件 [@noneflow](https://github.com/noneflow) ([#2889](https://github.com/nonebot/nonebot2/pull/2889))
|
||||
- Plugin: mai2_pcount [@noneflow](https://github.com/noneflow) ([#2891](https://github.com/nonebot/nonebot2/pull/2891))
|
||||
- Plugin: nonebot-plugin-ehentai-search [@noneflow](https://github.com/noneflow) ([#2885](https://github.com/nonebot/nonebot2/pull/2885))
|
||||
- Plugin: pokepoke_miss [@noneflow](https://github.com/noneflow) ([#2883](https://github.com/nonebot/nonebot2/pull/2883))
|
||||
- Plugin: 聊天截图伪造 [@noneflow](https://github.com/noneflow) ([#2880](https://github.com/nonebot/nonebot2/pull/2880))
|
||||
- Plugin: ba-tools [@noneflow](https://github.com/noneflow) ([#2867](https://github.com/nonebot/nonebot2/pull/2867))
|
||||
- Plugin: 精华消息管理 [@noneflow](https://github.com/noneflow) ([#2873](https://github.com/nonebot/nonebot2/pull/2873))
|
||||
- Plugin: B站收藏夹监视器 [@noneflow](https://github.com/noneflow) ([#2869](https://github.com/nonebot/nonebot2/pull/2869))
|
||||
- Plugin: Alist [@noneflow](https://github.com/noneflow) ([#2865](https://github.com/nonebot/nonebot2/pull/2865))
|
||||
- Plugin: 🦌管签到 [@noneflow](https://github.com/noneflow) ([#2859](https://github.com/nonebot/nonebot2/pull/2859))
|
||||
- Plugin: 漂流瓶 [@noneflow](https://github.com/noneflow) ([#2861](https://github.com/nonebot/nonebot2/pull/2861))
|
||||
- Plugin: 奇怪的小功能 [@noneflow](https://github.com/noneflow) ([#2851](https://github.com/nonebot/nonebot2/pull/2851))
|
||||
- Plugin: SunoAI音乐生成 [@noneflow](https://github.com/noneflow) ([#2853](https://github.com/nonebot/nonebot2/pull/2853))
|
||||
- Plugin: 谁是卷王 [@noneflow](https://github.com/noneflow) ([#2849](https://github.com/nonebot/nonebot2/pull/2849))
|
||||
- Plugin: GPT-SoVITS 语音合成 [@noneflow](https://github.com/noneflow) ([#2847](https://github.com/nonebot/nonebot2/pull/2847))
|
||||
- Plugin: 基于清影的AI视频生成 [@noneflow](https://github.com/noneflow) ([#2843](https://github.com/nonebot/nonebot2/pull/2843))
|
||||
- Plugin: 命令行 [@noneflow](https://github.com/noneflow) ([#2840](https://github.com/nonebot/nonebot2/pull/2840))
|
||||
- Plugin: exe_code [@noneflow](https://github.com/noneflow) ([#2835](https://github.com/nonebot/nonebot2/pull/2835))
|
||||
- Plugin: nonebot-plugin-autopush [@noneflow](https://github.com/noneflow) ([#2833](https://github.com/nonebot/nonebot2/pull/2833))
|
||||
- Plugin: vv_helper [@noneflow](https://github.com/noneflow) ([#2825](https://github.com/nonebot/nonebot2/pull/2825))
|
||||
- Plugin: nonebot_plugin_game_torrent [@noneflow](https://github.com/noneflow) ([#2827](https://github.com/nonebot/nonebot2/pull/2827))
|
||||
- Plugin: 每日油价 [@noneflow](https://github.com/noneflow) ([#2822](https://github.com/nonebot/nonebot2/pull/2822))
|
||||
- Plugin: wordle [@noneflow](https://github.com/noneflow) ([#2818](https://github.com/nonebot/nonebot2/pull/2818))
|
||||
- Plugin: 再润 [@noneflow](https://github.com/noneflow) ([#2816](https://github.com/nonebot/nonebot2/pull/2816))
|
||||
- Plugin: 漫展/展览查询 [@noneflow](https://github.com/noneflow) ([#2811](https://github.com/nonebot/nonebot2/pull/2811))
|
||||
- Plugin: 鸣潮wiki [@noneflow](https://github.com/noneflow) ([#2804](https://github.com/nonebot/nonebot2/pull/2804))
|
||||
- Plugin: cloudfare R2 客服端 [@noneflow](https://github.com/noneflow) ([#2806](https://github.com/nonebot/nonebot2/pull/2806))
|
||||
- Plugin: AnyMate小助手 [@noneflow](https://github.com/noneflow) ([#2761](https://github.com/nonebot/nonebot2/pull/2761))
|
||||
|
||||
### 🍻 机器人发布
|
||||
|
||||
- Bot: Minecraft_QQBot [@noneflow](https://github.com/noneflow) ([#2837](https://github.com/nonebot/nonebot2/pull/2837))
|
||||
- Bot: 星辰 Bot [@noneflow](https://github.com/noneflow) ([#2824](https://github.com/nonebot/nonebot2/pull/2824))
|
||||
|
||||
## v2.3.2
|
||||
|
||||
### 🐛 Bug 修复
|
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { useDocsVersionCandidates } from "@docusaurus/plugin-content-docs/client";
|
||||
import { PageMetadata } from "@docusaurus/theme-common";
|
||||
import { useDocsVersionCandidates } from "@docusaurus/theme-common/internal";
|
||||
import { useVersionedSidebar } from "@nullbot/docusaurus-plugin-getsidebar/client";
|
||||
import { SidebarContentFiller } from "@nullbot/docusaurus-theme-nonepress/contexts";
|
||||
|
||||
@@ -25,7 +25,7 @@ function StorePage({ title, children }: Props): JSX.Element {
|
||||
)!;
|
||||
|
||||
return (
|
||||
<Page hideTableOfContents reduceContentWidth={false}>
|
||||
<Page hideTableOfContents reduceContentWidth={false} sidebarId={SIDEBAR_ID}>
|
||||
<SidebarContentFiller items={sidebarItems} />
|
||||
<article className="prose max-w-full">
|
||||
<h1 className="store-title">{title}</h1>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user